Promises and handling multiple async operations
A Promise is a placeholder for a future value — pending → fulfilled or rejected, settled once. For multiple async ops: `Promise.all` (fail-fast parallel), `Promise.allSettled` (parallel, never rejects, returns per-result status), `Promise.race` (first to settle), `Promise.any` (first to fulfill, ignores rejections). Use `for await…of` or a worker pool when you need bounded concurrency.
A Promise is a wrapper around an async result — an object whose state moves from pending to either fulfilled (with a value) or rejected (with a reason). Once settled, it's immutable. .then queues a callback for fulfillment, .catch for rejection, .finally for either. async/await is syntactic sugar — every async function returns a Promise.
The four combinators — know when to reach for each.
Promise.all([a, b, c]) // fulfilled with [va, vb, vc]; rejects on first failure
Promise.allSettled([a, b, c]) // always fulfills with [{status, value|reason}, ...]
Promise.race([a, b, c]) // settles with whatever settles first (fulfill OR reject)
Promise.any([a, b, c]) // fulfills with first SUCCESS; rejects only if all fail (AggregateError)Decision rule.
- All must succeed →
Promise.all(and you'll catch the first error). - All should run, you want every outcome →
Promise.allSettled(analytics, batch operations). - Try multiple sources, take the fastest success →
Promise.any(mirror downloads, failover). - Timeout pattern →
Promise.race([fetch(), timeout(5000)]).
Parallel vs serial. Two common bugs:
// Bug — serial. Each await blocks the next.
for (const url of urls) {
results.push(await fetch(url));
}
// Fix — parallel.
const results = await Promise.all(urls.map(u => fetch(u)));But the parallel version unbounded can swamp the network with 10,000 requests. For bounded concurrency, use a small pool:
async function pool<T, R>(items: T[], n: number, fn: (t: T) => Promise<R>) {
const out: R[] = new Array(items.length);
let i = 0;
async function worker() {
while (i < items.length) {
const idx = i++;
out[idx] = await fn(items[idx]);
}
}
await Promise.all(Array.from({ length: n }, worker));
return out;
}This caps in-flight work at n — typical values 5–20 for HTTP, 1 for sequential DB writes.
The four senior details.
- Don't
awaitinsidePromise.all's map callback unless you mean it.urls.map(async u => { const r = await fetch(u); return parse(r); })is fine — each still runs in parallel, the awaits are independent.
- Unhandled rejections crash Node and log warnings in browsers. Always have a
.catchortry/catchat the outermost level. In React, an unhandled rejection in an effect won't trigger an error boundary unless you funnel it through state.
- **
Promise.allrejects fast but doesn't cancel siblings.** They still run. If you need cancellation, pass anAbortControllerand have each task respect the signal.
asyncfunctions always return a Promise, evenasync () => 1.returninsideasyncresolves with the value;throwrejects. There's no synchronous escape hatch.
Common micro-mistakes.
return await xis identical toreturn xin most cases — except insidetry/catch, wherereturn awaitlets the catch handle a rejection. Otherwise the wrapper function returns the rejected promise to the caller.new Promise((resolve) => doAsync(resolve))is fine for wrapping callbacks; never wrap an already-returning Promise innew Promise— that's the explicit-construction antipattern.- Promises are eager. The moment you create
fetch(url), the request fires. They are not lazy like generators.
Follow-up questions
- •Difference between Promise.all and Promise.allSettled — when to use each?
- •How does async/await desugar to .then chains?
- •Implement Promise.all from scratch.
- •What is the microtask queue and how does it interact with Promises?
Common mistakes
- •Serial awaits in a loop when parallel was intended.
- •Using `Promise.all` for batch operations where a single failure shouldn't abort the rest.
- •Forgetting that rejected siblings of Promise.all still run to completion.
- •Wrapping an existing Promise in `new Promise` (explicit construction antipattern).
Performance considerations
- •Unbounded parallelism (`Promise.all` on 10k items) can saturate connections; use a pool.
- •Microtask queue runs to exhaustion between macrotasks — heavy `.then` chains can starve rendering.
Edge cases
- •Empty array — `Promise.all([])` resolves to `[]` immediately; `Promise.any([])` rejects with AggregateError.
- •Mixing non-Promise values into combinators — they're wrapped via `Promise.resolve`.
- •Promises chained without `await` lose their context in async stack traces unless `--async-stack-traces` is on.
Real-world examples
- •Fetching N microservice calls on a page load: `Promise.all`.
- •Batch-uploading 1000 files with rate limiting: bounded pool.
- •Geo-fallback lookups: `Promise.any([cdn1, cdn2, cdn3])`.