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.