A dashboard endpoint fans out to five independent services with Promise.all, and one flaky service takes the whole page down. How do you decide whether to keep all-or-nothing or move to Promise.allSettled, and what has to change if you switch?
answer
- is a response with a hole still true
- independent panels versus one derived figure
- split required from optional
- unknown is not the same as empty
- you gave up your free error signal
basics
~20 sDecide from the invariant: keep Promise.all when the response is meaningless or misleading without every part, and move to Promise.allSettled when sections are genuinely independent. Switching changes the response contract, so the payload must mark which sections are degraded and the reasons must be logged.
solid answer
~50 sThe question is whether a response missing one section is still *correct*. If the five calls are independent widgets, partial results are strictly better than a blank page and `Promise.allSettled` wins. If they are components of one derived figure — a total, a permission decision, an invoice — then a silently missing part produces a confidently wrong answer, and fail-fast is the safer contract. Where the sets differ I split them: the required calls under `Promise.all`, the optional ones under `Promise.allSettled`. Switching is a contract change, not a one-line edit: the payload needs a per-section status so a consumer can tell "unknown" from "empty", the rejected reasons must be logged and counted because nothing propagates to my error reporting any more, and I have to be explicit that allSettled now waits for the slowest failure rather than returning at the first one.
go deeper
Know that the choice is about whether a partly-filled answer is still useful, and that Promise.allSettled is what lets you show the sections that did load instead of nothing at all.
Be able to argue both directions with concrete examples and to write the nested form that puts required calls under Promise.all and optional ones under Promise.allSettled.
Show that switching costs you the free error signal Promise.all provided, so logging and a failure metric per section become mandatory, and that allSettled waits for the slowest entry rather than bounding latency.
Own it as a contract decision across producers and consumers: define how degraded sections are represented, what threshold escalates to a hard error, and how the required/optional classification is recorded so it survives the next person adding a sixth call.
## Start from the invariant, not from the combinator The useful question is not "which combinator is nicer" but "is a response with a hole in it still true?" Two cases sit at opposite ends. **Independent panels.** Five widgets that a user reads separately — recent activity, news, usage chart, tips, notifications. A missing panel is a visible, bounded, honest degradation. Failing the entire page because the tips service is down trades a small loss for a total one. `Promise.allSettled` is right. **Components of one derived value.** An account total assembled from five balances, an authorization decision assembled from several sources, an invoice built from line-item services. Dropping one input does not produce a smaller answer, it produces a *wrong* answer that looks authoritative. Here `Promise.all`'s fail-fast is a correctness feature: the caller must not be handed a number it will act on. Most real endpoints are a mix, and the mix is the right design: ```js const [core, optional] = await Promise.all([ Promise.all([fetchUser(id), fetchPermissions(id)]), // required Promise.allSettled([fetchNews(), fetchTips(), fetchUsage(id)]), // optional ]); ``` The outer `all` still fails fast if the required set fails, while the optional set can degrade. That mapping — required versus optional — is a product decision, so write it down next to the code rather than inferring it from whichever combinator someone reached for. ## Switching is a contract change The most common mistake is treating the switch as a local refactor. Three things move with it. **The response shape.** Once sections can be missing, the consumer needs to distinguish "we know there are zero items" from "we could not find out". Returning `[]` or `null` for a failed section conflates them, and a UI that renders "0 notifications" during an outage is actively lying. Carry status per section: ```js const section = (r) => r.status === 'fulfilled' ? { state: 'ok', data: r.value } : { state: 'unavailable' }; ``` What you must not do is leak `reason` — an internal error message or stack — into a public payload. Log it server-side, expose a state flag and at most an opaque correlation id. **Observability.** `Promise.all` had a free property: a dependency failure produced a real error that reached logs, alerts, and error tracking. `allSettled` swallows that by design, so a dependency can fail one hundred percent of the time while every request returns 200 with a slightly thinner body. You have to replace what you gave up — log every `reason`, emit a per-section failure counter, and alert on the rate. Without that step you have not made the system more resilient, only quieter about being broken. **Latency.** `all` could return at the first rejection, sometimes fast. `allSettled` always waits for the slowest entry, so a dependency that hangs now sets your p99 instead of failing you early. That is why partial results and a per-call time bound belong to the same decision: the combinator determines what you do with a failure, but something else has to guarantee a failure arrives in bounded time. Nothing about `allSettled` makes a hanging call finish. ## The all-failed policy `allSettled` treats five failures exactly like one — you get an array where everything is rejected. Decide deliberately what that means. A dashboard where every section is unavailable should almost certainly be an error response, not a skeleton page; otherwise a total outage renders as a working but empty product and your availability metric never notices. Pick a threshold, and consider `AggregateError` (ES2021) to escalate with all the reasons attached. ## What stays true either way Neither combinator cancels anything, so the flaky service is still called, still consumes a connection, and still finishes its work in both designs. Neither retries. Neither imposes a limit on how many calls run at once. Moving to `allSettled` changes only how the failure is *reported* — everything else you want from a resilient fan-out has to be built alongside it. ## How to answer this in an interview Lead with the invariant test, give one example from each end, propose the split of required versus optional rather than a blanket answer, and then show that you know the switch has costs — contract, observability, tail latency. Candidates who answer "always use allSettled, it's more resilient" are describing a system that fails silently.
- If some calls are required and some are optional, how would you structure the fan-out?Nest the combinators: put the required calls in a `Promise.all` and the optional ones in a `Promise.allSettled`, then await both together. The required set keeps fail-fast semantics so a missing essential field can never reach the caller, while the optional set degrades individually. The required/optional split is a product decision, so it should be declared explicitly rather than implied by call-site style.
- What is the danger of representing a failed section as an empty array in the response?It erases the difference between "there are no items" and "we could not find out". Clients then render a confident zero during an outage, caches may store the empty answer as truth, and downstream aggregation counts it as a real value. Carry an explicit per-section state instead, and keep the underlying reason server-side in logs rather than in the payload.
- Does moving to Promise.allSettled improve the endpoint's latency?No — it usually worsens the tail. Promise.all could return at the first rejection, while allSettled waits for the slowest entry, so a hanging dependency now sets your p99 rather than failing you early. Partial results and a bounded per-call deadline are separate mechanisms, and you generally need both; the combinator decides what a failure means, not when one arrives.
saying these in an interview costs you the question
- Says allSettled is always the more resilient choice
- Renders a failed section as zero or empty data
- Forgets that swallowed reasons still need logging
- Assumes allSettled bounds latency for a hanging call
- Puts a required field in the optional partial-results set