Course outline

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.

About 4 min readContent reviewed
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.

javascript An explicitly recovered rejected stage
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);
Expected output
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.

javascript Independent partial outcomes
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);
Expected output
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.

Official references