How do you handle errors if multiple requests are sent simultaneously
Choose by semantics: `Promise.all` rejects on first failure — good for atomic ops. `Promise.allSettled` collects per-item outcomes — good for independent work. Retry transient failures with exponential backoff. Use AbortController to cancel siblings if one critical request fails. Surface partial-success states explicitly in UI.
Error handling in parallel requests comes down to what should happen when one fails.
Semantics first
| Situation | Helper | Failure behavior | |
|---|---|---|---|
| All must succeed | Promise.all | Reject on first; siblings still run but their results are lost. | |
| Partial success acceptable | Promise.allSettled | Always resolves with per-item `{status, value | reason}`. |
| First-to-succeed wins | Promise.any | Resolves on first success; AggregateError if all fail. | |
| First-to-settle (timeout) | Promise.race | Resolves/rejects on first settle. |
Pattern: independent best-effort
const results = await Promise.allSettled(loaders);
return results.map((r, i) =>
r.status === "fulfilled"
? { ok: true, data: r.value }
: { ok: false, error: r.reason, retry: loaders[i] },
);Render each card with success or per-card error + retry.
Pattern: atomic with cancel-on-fail
const ac = new AbortController();
try {
const results = await Promise.all([
fetch("/a", { signal: ac.signal }),
fetch("/b", { signal: ac.signal }),
fetch("/c", { signal: ac.signal }),
]);
} catch (err) {
ac.abort(); // cancel siblings still in flight; saves bandwidth
throw err;
}Retry transient failures
async function retry(fn, { retries = 3, baseMs = 200 } = {}) {
let last;
for (let i = 0; i <= retries; i++) {
try { return await fn(); }
catch (e) {
last = e;
if (!isTransient(e) || i === retries) throw e;
await new Promise((r) => setTimeout(r, baseMs * 2 ** i + Math.random() * 100));
}
}
throw last;
}Exponential backoff with jitter; retry only on transient errors (network, 5xx, 429), not 4xx.
Distinguish error kinds
- Network error — no response (offline, CORS, DNS).
- HTTP 4xx — client problem; don't retry blindly.
- HTTP 5xx — server problem; safe to retry.
- 429 — rate limited; honor
Retry-After. - AbortError — intentional cancel; treat as not-an-error.
Surfacing partial success
UI patterns:
- Per-item status (dashboard cards with their own error state).
- Banner "3 of 5 widgets failed to load".
- Toast for transient retries with auto-dismiss.
- Hard error for atomic operations (modal blocking next step).
Don't suppress
Either handle each rejection or let it propagate to a known error boundary. Suppressing with empty catch creates ghosts.
Interview framing
"Pick Promise.all for atomic ops (any failure invalidates the result), Promise.allSettled for independent best-effort work. For atomic ops with side-effecty calls, cancel siblings on failure with AbortController. Retry transient failures (network, 5xx, 429) with exponential backoff + jitter — never 4xx. Surface partial success explicitly: per-card errors, a 'X of Y failed' banner, not a silent half-broken UI. Treat AbortError as not-an-error in your catch."
Follow-up questions
- •What's exponential backoff with jitter?
- •Why is 4xx not retriable?
- •How would you implement a circuit breaker?
Common mistakes
- •Promise.all where allSettled is right (or vice versa).
- •Retrying 4xx errors.
- •Treating AbortError as a real failure.
- •Silent catch.
Performance considerations
- •Retry storms can make outages worse — jitter and a circuit breaker matter at scale.
Edge cases
- •Rate-limited 429 with Retry-After header.
- •User cancels mid-batch.
- •Server returns mixed results in one response (different error shapes).
Real-world examples
- •AWS SDK retry policies, axios interceptors, React Query retry strategy.