Before this lesson

Understand functions, callbacks and try/catch. Complete the scope lesson first.

What you will be able to do

Predict synchronous code, promise reactions and timers, and correctly await failures.

Understand the mechanism

Synchronous code in the current task runs first. Promise reactions and await continuations use microtasks, processed at checkpoints before the next task. A timer delay is an earliest scheduling threshold, not an exact execution time; a busy thread delays it.

A promise is pending, fulfilled or rejected; settlement is final. then returns a new promise. Returning a value passes it onward, while throwing rejects the next promise. Forgetting to return inner asynchronous work allows the outer chain to finish too soon.

async functions always return promises. await suspends that async function, not the whole page. Await independent work with Promise.all and dependent work sequentially. Promise.all rejects on failure but does not automatically cancel other operations.

Runnable example

console.log("A");
setTimeout(() => console.log("D"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("B");
async function calculate() {
  try {
    const n = await Promise.resolve(21);
    return n * 2;
  } catch (error) {
    console.error(error);
    throw error;
  }
}
calculate().then(value => console.log(value));

Trace and verify

Expect A, B, C, 42, D. The first two are synchronous; promise and await continuations run before the timer. Replace the resolved value with a rejected Error and add a final catch to observe propagation.

Common mistakes and boundaries

forEach does not await async callbacks. Use for...of with await for sequential work, or map plus Promise.all for concurrency. An empty catch hides failure and can make later code appear successful.

Practice challenge

Create two delayed numeric tasks and sum them concurrently. Make one fail and ensure a total is shown only on success.

Reference solution and reasoning

Await Promise.all inside try, sum after successful resolution, and display failure in catch. finally is for cleanup, not unconditional success messaging.

Lab: errors must remain on the promise chain

Returning a promise and merely starting it inside a callback give the caller different completion guarantees. Understand error propagation with local tasks before adding network uncertainty. Each catch below returns a fallback so you can observe how recovery changes the rest of the chain.

Run this separate experiment in a modern browser Console; it is not a replacement for the main example above.

async function loadNumber(text) {
  const value = Number(text);
  if (text.trim() === '' || !Number.isFinite(value)) throw new Error('Invalid number');
  return value;
}
async function experiment() {
  console.log('start');
  const task = loadNumber('oops')
    .then(value => value * 2)
    .catch(error => { console.log(error.message); return 0; })
    .then(value => value + 1);
  console.log('scheduled');
  console.log(await task);
  console.log('end');
}
experiment();

Expected observations

Output is start, scheduled, Invalid number, 1, end. Rejection skips the first then; returning 0 from catch restores a fulfilled path for the next then.

Why this happens and where it stops applying

catch changes control flow. Logging without rethrowing may make callers interpret failure as success, possibly with undefined as the value. finally is for cleanup such as clearing loading state; throwing there replaces the previous outcome. Promise.all rejects when an input rejects but does not automatically cancel the remaining work. Aggregation and cancellation are separate responsibilities.

Extension challenge

If no meaningful fallback exists, how can you log an error while keeping failure visible to the caller?

Reasoned solution

Log and throw error again, then handle the rejection at the outer UI boundary. Give success, failure and cancellation distinct observable states. Turning every exception into an empty array disguises failures as legitimate empty results.

Further reading