Async patterns you will use at work
Real services fan work out. They load ten feeds, call three APIs, and accept that some of those calls fail or hang.
The tools from the last two lessons compose into a small set of patterns that cover most production async code, and most senior-sounding interview answers.
This lesson works through three of them: partial failure, timeouts, and looping over async work without the classic footgun.
Pattern 1: partial failure.
Promise.all is the wrong tool when one failure should not sink the whole batch, because it rejects on the first error and discards everything else.
Promise.allSettled waits for every input and reports each outcome as { status: "fulfilled", value } or { status: "rejected", reason }.
So you can render nine widgets plus one error message instead of a blank page, which is almost always the better product decision.
The cost is that you now own the inspection. allSettled never rejects, so nothing forces you to look at the statuses, and code that ignores them silently treats failures as successes.
Reporting every outcome
One input fails and the other two still arrive.
function fetchUser(id) { return new Promise((resolve, reject) => setTimeout(() => { if (id === 2) reject(new Error("user 2 missing")); else resolve("user" + id); }, 10) ); } async function main() { const results = await Promise.allSettled([fetchUser(1), fetchUser(2), fetchUser(3)]); for (const r of results) { if (r.status === "fulfilled") console.log("ok: " + r.value); else console.log("failed: " + r.reason.message); } } main();
Output
ok: user1
failed: user 2 missing
ok: user3User 2 fails, and users 1 and 3 come through. allSettled itself never rejects, so you inspect each result's status yourself.
The results stay in input order like Promise.all, which is what lets you line them up against the ids you asked for.
r.value exists only on fulfilled entries and r.reason only on rejected ones, so reading the wrong one gives undefined. Checking status first is not optional.
Replacing allSettled with all here would print nothing at all. The combined promise would reject at 10 ms and the loop would never run.
Counting failures afterward is the natural next step, and results.filter((r) => r.status === "rejected").length is the whole implementation.
Pattern 2: timeouts with race
Nothing in the language times a promise out for you.
A network call that never settles would leave an await hanging forever, and the surrounding function would simply never resume.
The standard fix is Promise.race([work, timeout]). Race settles with whichever input settles first, so pairing the real work with a promise that rejects after a deadline turns a hung call into a catchable error.
Losing the race changes nothing about the loser. The slow request keeps running to completion and its result is discarded, which is the same non-cancellation caveat as fail-fast all.
The timer also keeps running when the work wins, and a pending setTimeout can hold a Node process open. Production versions clear the timer in a finally.
For actual fetch calls the language now offers something better. AbortSignal.timeout(ms) passed as a signal cancels the request for real, and race remains the general-purpose answer for promises you do not control.
timeout
A promise whose only job is to reject on a deadline.
function delay(ms, value) { return new Promise((resolve) => setTimeout(() => resolve(value), ms)); } function timeout(ms) { return new Promise((resolve, reject) => setTimeout(() => reject(new Error("timed out")), ms) ); } async function main() { const fast = await Promise.race([delay(10, "fast result"), timeout(50)]); console.log(fast); try { await Promise.race([delay(100, "slow result"), timeout(30)]); } catch (err) { console.log(err.message); } } main();
Output
fast result timed out
timeout copies delay's shape and uses the executor's second parameter, calling reject instead of resolve.
The first race is won by the 10 ms work, so await produces the value and the 50 ms timer never matters.
The second race is won by the 30 ms deadline, so the rejection surfaces as a throw and the try/catch reports it.
The try/catch is required on the second one and pointless on the first. A race that a rejecting promise can win needs a handler, which is easy to forget when the happy path is usually fast.
Wrapping this into a helper is the natural refactor, and withTimeout(promise, ms) returning Promise.race([promise, timeout(ms)]) is a one-liner worth keeping.
Pattern 3: the forEach footgun
array.forEach(async (item) => ...) looks like it awaits each item, and it does not.
forEach ignores its callback's return values, so it fires every async callback and returns immediately. The awaits pause the callbacks, not your function.
This bug ships to production constantly, and the reason is that it usually still works. The operations complete, just not in order and not before the surrounding function moves on.
The symptoms are recognizable once you know them. A function that reports success before its work finishes, a rate limit tripped by a hundred simultaneous requests, or a test that passes locally and fails in CI.
Two working rules cover it.
When steps must run one at a time, as with rate limits or ordered writes, use for...of with await in the body.
When the items are independent, build the promises with map and await Promise.all(...), which is lesson 6-2's pattern.
The same trap applies to map used for side effects. array.map(async ...) returns an array of promises, and forgetting to await that array leaves the work unwaited exactly as forEach does.
forEach and for...of compared
The same two items looped two ways.
function delay(ms, value) { return new Promise((resolve) => setTimeout(() => resolve(value), ms)); } async function withForEach() { [1, 2].forEach(async (id) => { const user = await delay(20, "user" + id); console.log(user); }); console.log("forEach returned before any user loaded"); } async function withForOf() { for (const id of [1, 2]) { const user = await delay(20, "user" + id); console.log(user); } console.log("for...of waited for both"); } withForEach(); setTimeout(withForOf, 100);
Output
forEach returned before any user loaded user1 user2 user1 user2 for...of waited for both
The forEach version returns before a single user has loaded, and its callbacks run in parallel.
The for...of version genuinely waits for each item, and it is started 100 ms later here so the two outputs do not interleave.
The giveaway in the output is the position of each summary line. forEach prints its line first and for...of prints its line last.
withForEach also has no way to fail visibly. An error inside one of those callbacks becomes an unhandled rejection, since nothing holds the callback's promise.
Timing separates the two clearly. The forEach version finishes its work in about 20 ms and reports at 0 ms, while for...of reports at 40 ms after both items are actually done.
The right tool is Promise.allSettled.
It waits for everything and reports a status per item, which is exactly what a results screen needs.
Promise.all rejects at the first failed upload and discards the rest, so nine successful uploads would be invisible.
Promise.race settles after just one input, so it answers a completely different question.
forEach with an async callback cannot make the surrounding function wait at all, so there would be nothing to report when the handler returned.
Promise.any is the remaining option and it is wrong here too, since it resolves with the first success and ignores the other nine outcomes.
The tell in the question is the phrase "every single one even if some fail". Any requirement that mentions reporting per-item outcomes points at allSettled.