A customer summary page needs a profile and a list of recent orders. If the two requests are independent, the application can start both before waiting for their results. If the orders query needs an account identifier from the profile, that dependency changes the schedule: the second request cannot begin correctly until the identifier is available.
Asynchronous programming gives a program ways to represent and coordinate work that completes later. It can let other ready work progress during an I/O wait, depending on the runtime and operation. It does not remove dependencies or supply unlimited capacity.
The useful questions are when work starts, what it waits for, who owns its completion, and what happens when it fails. Answering those questions makes promises and async/await easier to reason about than the broad claim that asynchronous code does several things at once.
Distinguish Concurrency From Parallel Execution
Concurrent tasks have overlapping lifetimes. One can wait for a network response while another runs application code. Parallel execution means work is executing simultaneously, for example on different processor cores. An application can be concurrent without executing its JavaScript on multiple cores.
The examples here use JavaScript promises and async/await. Other runtimes have different scheduling rules. For example, Python’s asyncio documentation distinguishes coroutine objects from scheduled tasks. Calling an asynchronous-looking function does not have one universal start behaviour across languages.
In JavaScript, an async function returns a promise. Its initial body runs synchronously until it reaches suspension, and await arranges continuation after the awaited value is settled. Marking a CPU-heavy loop async does not move that loop onto another processor.
An event loop can service ready work while an operation waits through supported mechanisms. Blocking code still delays other work on that execution thread. A synchronous file read or a long calculation does not become non-blocking merely because its caller is an async function.
Make the Dependency Graph Explicit
For independent operations, starting both before awaiting their joint result permits overlap. For dependent operations, preserving the order is necessary. Syntax should reflect that distinction rather than replacing every sequence of awaits with a concurrent batch.
This function accepts two operations that return promises. It starts them as part of constructing the batch and then waits for both results:
async function loadSummary(readProfile, readOrders) {
const [profile, orders] = await Promise.all([
readProfile(),
readOrders(),
]);
return { profile, orders };
}
The example assumes both operations are independent and appropriate for the same authorised customer context. The returned object is produced only after both succeed. Promise.all preserves input order in its result array, regardless of which operation finishes first.
For an illustrative timing comparison, assume one operation takes 500 milliseconds and the other 200, with no relevant contention or additional overhead. Sequential execution takes 700 milliseconds; starting them together can bring the combined wait toward 500. These assumed durations demonstrate scheduling, not measured application performance.
| Relationship | Appropriate coordination | What must remain true |
|---|---|---|
| Independent profile and order reads | Start both, then await required results | Each has valid inputs and permissions |
| Orders need an identifier from the profile | Await profile before requesting orders | The dependency cannot be guessed |
| Optional recommendation alongside required data | Define partial-result handling | A failed optional result has a clear fallback |
| Many independent remote requests | Limit active concurrency | Downstream capacity is respected |
A dependency graph also helps identify unnecessary serial work. Retrieving permissions may be required before exposing orders, but unrelated display metadata may be fetched concurrently if its own access and failure rules permit it.
Define Failure Before Adding More Concurrency
Promise.all rejects when one input rejects. It does not automatically cancel the other operations. If the profile request fails while the order request is still running, the order operation can continue even though the combined result will not be returned successfully.
Choose whether all results are required. A required authorisation check cannot become optional simply because another request completed. An optional recommendation may be omitted if the product explicitly supports that degraded result. Promise.allSettled can help inspect all outcomes, but it does not decide which failures are acceptable.
Handle rejections at a boundary that can make the relevant decision. Silently catching every error and returning an empty list can make unavailable data look like a genuine absence of orders. Preserve the distinction between no matching records and a failed read.
The timing of a failure can also affect resource ownership. Work already started needs to be observed or cancelled where supported, rather than left without an owner. A short-lived request handler should not create background promises whose eventual failures nobody records or resolves.
Cancellation Is a Request With a Scope
A deadline limits how long the caller is willing to wait. It does not prove that a remote operation stopped or that an external effect did not happen. The application needs to distinguish a confirmed failure from an uncertain result.
AbortController provides cancellation signalling for supported web operations such as fetch. Passing its signal to an operation matters; creating a controller without connecting it does not control that work. Cancellation support also needs to be understood at each boundary.
A search interface can use cancellation to reduce obsolete work, while still checking that a result belongs to the latest query before displaying it. A payment operation needs stronger effect handling because aborting the client’s wait does not necessarily stop the server from processing the payment.
Propagate a useful deadline through dependent calls and leave time for cleanup or recovery where required. Replacing one bounded wait with several independent generous timeouts can make the complete operation exceed its intended budget. The article on why timeouts do not cancel work explores the distributed boundary further.
Bound Work as Well as Waiting
Launching ten thousand promises does not create ten thousand units of downstream capacity. It can exhaust connections, fill client memory, trigger rate limits, or shift a large backlog into a remote service. Concurrency control determines how much work may be active at once.
Use a bounded worker group, semaphore, or equivalent mechanism suited to the runtime. Bound queued work as well as active work where demand can exceed capacity for a sustained period. Decide whether excess requests should wait, be rejected, or use another path.
Consider cost differences between tasks. A fixed request count may be a poor limit if one operation performs a small lookup and another generates a large export. Observe the resource that constrains the workload rather than assuming every task is interchangeable.
Shared state still needs protection. Two async handlers can each read an old value, suspend, and later overwrite each other’s update. JavaScript running one callback at a time does not make a multi-step read-modify-write sequence atomic across awaits. Use an appropriate conditional update or transactional operation at the authoritative store.
Bounded concurrency also needs fairness when several callers share it. One large batch can occupy every slot while small interactive requests wait. Separate budgets or an appropriate scheduling policy can help when those workloads have different service requirements. Measure queue waiting as well as execution time to see whether overlap actually improves the intended user experience.
Keep Process-Local Async Separate From Durable Work
A promise represents eventual completion within the programming model; it does not persist a business obligation across a process crash. Starting an unawaited export in a web handler can lose the work when the environment is replaced.
For a durable job, record the intended work using a suitable queue or workflow mechanism and return an operation reference. Define retention, retries, status, and duplicate-effect handling. An asynchronous API response is a contract about accepted work, not merely evidence that a JavaScript function returned quickly.
HTTP 202 Accepted indicates acceptance for processing rather than successful completion. A useful application design supplies a way to follow progress and distinguish completed, failed, pending, and uncertain outcomes where appropriate.
This is particularly visible in serverless state management. Replaceable workers can continue a durable operation only when the necessary intent and progress survive outside a particular execution environment. Local concurrency and cross-process recovery solve different parts of the problem.
Test the Order and the Outcomes
Use controllable operations in tests so completions can be released in different orders. For loadSummary, verify that both reads begin before either is released, that reversed completion still produces the correct fields, and that a rejection reaches the caller rather than appearing as empty data.
Add checks for the application’s actual cancellation and capacity policy. Confirm what happens when an operation ignores cancellation, when a dependency is slow, and when the permitted concurrency is exhausted. Timing assertions based only on a fast local run can miss these conditions or become flaky.
Observe waiting time, active work, queue age, and confirmed results in operation. Async is useful when it coordinates those concerns without obscuring them. The goal is an execution model in which independent work can overlap while dependencies, ownership, and completion remain explicit enough to verify.





