Course outline

JavaScript / Intermediate: modules and asynchronous contracts / Lesson 9

Async workflows, sequencing, and cleanup

Use async and await to express dependencies without losing concurrency or errors. Compare sequential work, independent work, and cleanup around a controlled local failure.

About 4 min readContent reviewed
On this page

Lesson goals

  • Explain the promise returned by an async function.
  • Choose sequential or concurrent start deliberately.
  • Use try/catch/finally with awaited operations.

Await pauses this continuation

An async function always returns a promise, even when its source returns a plain value. await observes a value or thenable and resumes the function through asynchronous continuation; it does not block the entire browser thread waiting for I/O. Code before the first suspension executes during the call, while the caller receives a promise. A thrown error becomes rejection of that promise. Treat async syntax as a clearer way to compose promise dependencies, not as a switch that makes CPU-intensive loops nonblocking. A large synchronous calculation inside an async function still occupies its execution thread.

Sequence dependent stages

A receipt cannot be formatted from an enrollment identity until creation has succeeded. Await the required result, validate it, then continue. In the example, the first stage supplies a synthetic identifier and the second uses it; the returned receipt is still delivered through the async functionโ€™s promise. Keep the workflow result distinct from progress display so another caller can reuse it. Do not wrap each stage in a catch that invents a normal value, because later work might then use an invalid identity and produce a convincing but false confirmation.

javascript A dependent workflow returns a promise
async function createEnrollment() { return { id: "E-7" }; }
async function receipt() {
  const enrollment = await createEnrollment();
  return "receipt:" + enrollment.id;
}
console.log(await receipt());
console.log(receipt() instanceof Promise);
Expected output
receipt:E-7
true

Await obtains the enrollment before composing the receipt. Calling receipt without await still returns a Promise.

Start independent work before awaiting both

If two operations do not depend on one another, calling and awaiting one before starting the other imposes avoidable sequencing. Start both and await an appropriate aggregate when the contract permits overlap. Starting concurrently does not guarantee parallel JavaScript execution or a speedup; the host and resources determine that. Limit fan-out for large inputs so an array of thousands does not overwhelm memory or a dependency. Array forEach does not await async callbacks. Use for...of with await for intentional sequence, or map to promises and await an aggregate for intentional overlap.

Finally owns cleanup, not a new outcome

Use try/catch around an awaited rejection when the current layer owns recovery. finally is useful for releasing a busy state or resource, and should generally avoid returning a replacement value that hides an earlier result or error. The example records start, catches an awaited failure, and always restores the idle marker. If a function merely returns a rejecting promise inside try without awaiting it, a local catch does not observe that later rejection. Decide whether local cleanup must finish after settlement or whether the caller owns the remaining promise lifecycle.

javascript Awaited failure and deterministic cleanup
const events = [];
async function run() {
  events.push("busy");
  try {
    await Promise.reject(new Error("unavailable"));
  } catch (error) {
    events.push(error.message);
  } finally {
    events.push("idle");
  }
}
await run();
console.log(events.join(","));
Expected output
busy,unavailable,idle

The rejection enters catch, and finally restores the final state. The example intentionally recovers instead of rethrowing.

Results still need lifecycle and race rules

An awaited result may arrive after the user changes the selected course or closes a view. async syntax does not solve stale updates. Associate work with a request generation or owned cancellation signal, and check that the result still belongs to the active state before rendering. Cancellation can reduce wasted work only when the operation supports it; a stale-result guard remains useful. Keep loading, success, and error as defined states and ensure an older finally does not clear the busy state of a newer request. These are application coordination rules, not quirks fixed by adding more await keywords.

Common errors and fixes

forEach finishes before its async callbacks

Why it happens
Its return contract does not await callback promises.
How to fix it
Choose sequential for...of or awaited map plus aggregate.
Prevent it next time
Assert completion and rejection of the whole workflow.

A local catch misses a rejection

Why it happens
The promise was returned without being awaited inside the try.
How to fix it
Await where the layer needs to observe and recover.
Prevent it next time
Test a rejecting dependency, not only successful returns.

An old request clears a new loading state

Why it happens
Cleanup ignores request ownership.
How to fix it
Guard both result rendering and cleanup with current ownership.
Prevent it next time
Test explicit reverse completion order.

Check your understanding

1. A profile and a catalog are independent. How should their loading begin?

Show worked solution

Start both operations, then await all if both are required or allSettled if partial outcomes are supported. Explain that this overlaps asynchronous waiting rather than promising parallel JavaScript. Add bounded concurrency if the same pattern expands to a large collection.

2. A CPU-heavy loop is moved into async function compute(). Why can typing still freeze?

Show worked solution

The loop remains synchronous work on the same thread; declaring async does not offload it. Measure its duration, reduce work, split it across appropriate tasks if useful, or use a worker boundary for suitable independent computation. Awaiting an already fulfilled promise repeatedly is not a reliable rendering yield.

Key takeaways

  • Await expresses a dependency while preserving promise semantics.
  • Concurrency, cancellation, and state ownership remain explicit design decisions.

Official references