JavaScript / Intermediate: modules and asynchronous contracts / Lesson 8
Promises and asynchronous composition
Model a future result without confusing it with a running thread. Trace promise states, chaining, rejection, and aggregate results using deterministic local operations.
On this page
Lesson goals
- Trace fulfillment and rejection through a chain.
- Return promises so work remains connected.
- Select aggregation by result and failure policy.
A promise represents a result, not a thread
A promise begins pending and settles once as fulfilled or rejected. Settling does not imply that a separate thread executed its work. The constructor’s executor runs synchronously, while reaction handlers registered with then run through asynchronous job scheduling. Resolving a promise can supply a plain value or adopt another promise’s eventual outcome; resolved and fulfilled are not synonyms. Think in terms of dependencies: which operation can begin now, which result is needed next, and who will observe failure? This is more useful than describing every promise as background work. Host APIs determine what actual I/O or external activity occurs.
Return the next dependency
then returns a new promise. Returning a value fulfills that next promise with the value; throwing rejects it; returning a promise connects the next stage to that promise’s eventual result. Forgetting a return can create detached work and an undefined value downstream. The example transforms a known price through a chain and then deliberately throws a domain error, which the rejection handler turns into an explicit display result. The catch handler is also a transformation: if it returns normally, the chain after it is fulfilled, so recovery must be intentional rather than silent concealment.
const result = await Promise.resolve(200)
.then(cents => cents + 50)
.then(cents => { throw new Error("price unavailable"); })
.catch(error => "notice: " + error.message);
console.log(result);
notice: price unavailable
The thrown Error becomes rejection of the next promise. catch returns a message, so the final awaited result is a string.
Composition encodes failure policy
Promise.all waits for all fulfillments and preserves input order in its result, but rejects when an input rejects. That rejection does not cancel the remaining operations. Promise.allSettled waits for every outcome and describes each as fulfilled or rejected, useful when a page can report independent partial results. Promise.race follows the first settlement, while Promise.any seeks the first fulfillment and rejects if all inputs reject. Choose from the application contract, not just the shortest syntax. A checkout requiring both authorization and inventory has different failure needs from a dashboard showing independent informational cards.
Read each outcome instead of guessing success
For a small course dashboard, a title result can succeed while an optional badge fails. allSettled allows the renderer to show the title and an explicit unavailable badge state. Do not substitute a successful empty value for a failed required operation, because callers may mistake absence for a legitimate empty dataset. In the example, settled outcomes are converted into a small diagnostic list without exposing a full error stack. If inputs are already fulfilled promises, their handlers still follow promise scheduling; the aggregate does not make reaction callbacks synchronous.
const outcomes = await Promise.allSettled([
Promise.resolve("DOM"),
Promise.reject(new Error("badge offline"))
]);
console.log(outcomes.map(x => x.status).join(","));
console.log(outcomes[0].value);
console.log(outcomes[1].reason.message);
fulfilled,rejected
DOM
badge offline
Input order is preserved, and the failure remains distinct from a valid missing badge. All rejection observers are attached by the aggregate.
Every launched operation needs an owner
A function that starts asynchronous work should usually return its promise so the caller can await completion, handle rejection, and decide when UI cleanup is safe. Intentional detached work needs an explicit error observer and lifecycle policy; writing void before a call does not magically handle rejection. Avoid constructing a new Promise around an API that already returns one. Extra wrappers can lose errors and obscure cancellation, which belongs to a supporting operation rather than to Promise itself. Preserve causal context when adding error information, and keep sensitive request data out of user-visible diagnostics.
Common errors and fixes
A chain receives undefined unexpectedly
- Why it happens
- A callback starts work but does not return its promise or value.
- How to fix it
- Return the intended next-stage result.
- Prevent it next time
- Test the final value and error path of the whole chain.
catch silently converts failure into success
- Why it happens
- The handler logs and returns normally without a recovery contract.
- How to fix it
- Return a deliberate fallback or rethrow a meaningful error.
- Prevent it next time
- Document whether a catch recovers or propagates.
Promise.all is expected to cancel other requests
- Why it happens
- Rejection propagation is confused with operation cancellation.
- How to fix it
- Use supported cancellation and still handle later outcomes.
- Prevent it next time
- Keep cancellation policy explicit at the host API boundary.
Check your understanding
1. Two optional panels can fail independently. Which aggregate fits?
Show worked solution
Use allSettled when the UI should inspect both outcomes before rendering independent success or unavailable states. Preserve each reason privately and provide safe user messages. If one panel is required for the task, partial display needs an explicit product decision rather than automatic acceptance.
2. A then callback calls save() but does not return it. What can break?
Show worked solution
The next stage can proceed with undefined before save settles, and its rejection can escape the chain. Return save() so completion and failure become dependencies. Test with a controlled rejected save to verify the caller observes the error before showing confirmation.
Key takeaways
- Promises compose dependencies and outcomes; they do not create threads.
- A handled rejection is recovery only when the contract says so.