A promise represents an eventual fulfillment or rejection. Calling an async function immediately returns a promise; its body begins running until it reaches a suspension point. await observes a promise's eventual result and resumes the function later. A rejection becomes a thrown value at the await expression, so ordinary try/catch can define the recovery policy.
async function example() {
console.log("start");
await Promise.resolve();
console.log("resume");
}
example();
console.log("outside");The output is start, outside, resume. Awaiting an already fulfilled promise still suspends the continuation. Promise reactions run after the current synchronous work; this does not imply that timers and I/O callbacks have one universal ordering across all program contexts. The exercises use promises and controlled resolution rather than racing unrelated timers.
Sequential and concurrent work have different contracts. A loop that awaits each worker starts the next item only after the current one settles. If a failure should stop later work, let that rejection escape. A loop that merely pushes worker promises starts more work before earlier operations finish and returns a collection of promises instead of settled values.
forEach does not await async callbacks. Marking the callback async does not change forEach's return contract. Prefer an explicit loop for sequential work, or construct a promise array and await Promise.all for independent operations. Choose deliberately according to dependencies and failure policy.
async function series(values) {
const result = [];
for (const value of values) result.push(await Promise.resolve(value * 2));
return result;
}
series([3, 1]).then(values => console.log(JSON.stringify(values)));Promise.all preserves the input ordering of results, even when operations complete in another order. It rejects if an input rejects, and it does not cancel operations that are already running. If each failure should become an outcome record instead, catch inside each operation before aggregating. This is the policy of settleAll: every input position has a success or failure record.
A synchronous throw from a worker and an asynchronous rejection both need coverage. An async wrapper converts a synchronous throw into a rejected promise, and its local catch can turn either failure into data. Do not forget the await inside that try: returning an unobserved worker promise lets rejection happen outside the intended catch boundary.
Test causality, not speed. Keep an event list for series execution and verify start/end pairs. For concurrent settlement, create promises whose resolver functions the test controls; resolve the second before the first and check that result positions still match the inputs. These are real promise operations inside Node, but they are local deterministic fixtures. HTTP, cancellation, retries, timeouts and production concurrency limits require additional interfaces beyond this introductory lesson.