Three uploads are running under a single Promise.all and the second one rejects after 100 ms, so the await throws right away. What is happening to the other two uploads, and what problems does that create?
answer
- a handle on work, not a task
- the caller stops listening, work continues
- side effects land after your error path
- cleanup races the stragglers
- waiting and stopping are different fixes
basics
~20 sThey keep running to completion. Promise.all's fail-fast rejection only settles the aggregate promise; it has no power to stop work already in flight, so the other uploads still finish, still write their side effects, and their outcomes are silently discarded.
solid answer
~50 sNothing stops. A promise is a handle on work that has already started, not a task you can call off, so rejecting the aggregate only means *you* stopped listening. Both remaining uploads run to completion and their side effects — bytes written, rows inserted, quota consumed — land after your error handler has already reported failure. Their results are thrown away, and if one of them also rejects that reason is dropped too, though it does not surface as an unhandled rejection because `Promise.all` attached handlers to every input up front. The practical damage is partial state you did not intend and late writes racing your cleanup. If I need the batch genuinely finished before continuing, I use `Promise.allSettled` so I wait for every entry; if I need the work to actually stop, that has to be built into the operations themselves via an abort mechanism they check.
go deeper
Know the headline fact and say it confidently: a rejected Promise.all does not cancel anything, the other work still finishes. Understand that a promise represents a result you observe, not a job you control.
Explain why cancellation is impossible here — the promises were created before the combinator saw them and there is no channel back to the operation — and note that later rejections are swallowed rather than reported as unhandled.
Reason about the state the system is left in: partial writes, cleanup racing in-flight stragglers, retries duplicating work that quietly succeeded, and resources held longer than the error path implies. Show that waiting (allSettled) and stopping (an abort signal the operation checks) solve different halves of the problem.
Own the recovery contract for the whole fan-out: decide whether the operation must be atomic, idempotent, or eventually reconciled, and pick the combinator to match. Establish that any operation permitted into a fan-out has a defined behaviour when its result is abandoned.
## Fail-fast settles the aggregate, nothing else `Promise.all` rejects as soon as any input rejects. That is a statement about the *aggregate promise* only. Under the hood the combinator subscribed to each input; when one rejects it forwards the reason and marks itself settled. The subscriptions to the other inputs still exist, those operations are still executing, and the only thing that changed is that no one will do anything with their answers. This follows directly from what a promise is. By the time `Promise.all([upA(), upB(), upC()])` runs, all three functions have already been called and all three uploads are in flight. The array holds three read-only handles on results. There is no channel from a handle back to the operation, and the `Promise` API deliberately has no `cancel` method. ```js try { await Promise.all([upload(a), upload(b), upload(c)]); } catch (e) { // reached at ~100ms; uploads a and c are still transferring right now } ``` ## What actually goes wrong **Partial state.** The two surviving uploads complete and commit their effects — objects appear in the bucket, rows appear in the table, a counter increments — after the caller has already told the user the operation failed. Now the system holds a state that no code path intended: two of three, with no record of the third. **Cleanup races the stragglers.** The natural reaction to the failure is compensating work: delete what was uploaded, roll back, retry the whole batch. That cleanup runs *while* the survivors are still finishing, so a delete can execute before the write it was meant to undo, leaving the very object it tried to remove. Retrying the whole batch can duplicate the work that quietly succeeded, which is why idempotency keys matter here. **Lost diagnostics.** If a second input also rejects, that reason vanishes: the aggregate already settled, and a promise settles once. It does not appear as an unhandled rejection, because `Promise.all` attached handlers to every entry when it was called — so the error is genuinely handled, just handled by being discarded. In an incident you see one error and have no idea the other two also failed. **Resources stay held.** Sockets, file handles, and memory buffers belonging to the in-flight operations stay allocated until they finish naturally. Under a load spike where the first failure is fast, you can end up with far more concurrent work outstanding than your error path suggests. ## Waiting versus stopping — two different fixes These are separate needs and it is worth naming which one you have. If you need every operation to be **finished** before you continue — so cleanup is safe, or so a summary is accurate — use `Promise.allSettled`. It waits for all entries and hands you a per-entry verdict, so you know exactly which two landed: ```js const results = await Promise.allSettled(files.map(f => upload(f))); const uploaded = results .map((r, i) => (r.status === 'fulfilled' ? files[i] : null)) .filter(Boolean); if (results.some(r => r.status === 'rejected')) await rollback(uploaded); ``` Now the rollback knows precisely what exists, and no straggler can write after it. If you need the work to genuinely **stop**, the operations themselves must cooperate: they have to accept a signal, check it, and abandon their work. No combinator can add that from the outside — `Promise.all` cannot reach into a request that is already on the wire. ## Interview framing The question is really testing whether you understand that promises model *results*, not *tasks*. Candidates who say "the other uploads get cancelled" have the wrong mental model, and that model produces real bugs: cleanup logic written on the assumption that nothing else is still running. Say plainly that fail-fast is about when the caller learns of failure, not about what the system is still doing, then talk about the state you are left holding and how you reconcile it.
- If a second input rejects after Promise.all has already rejected, do you get an unhandled rejection warning?No. `Promise.all` attaches handlers to every input the moment it is called, so every rejection is technically handled — the aggregate simply ignores everything after the first. That is convenient because your console is not flooded, but it also means the later failures are invisible. If you need them, wrap each input in its own `.catch` that logs, or use `Promise.allSettled` and inspect the reasons.
- How would you rewrite the batch so the cleanup knows exactly what succeeded?Switch to `Promise.allSettled` so you wait for every entry, then map the result array back onto the input array by index: fulfilled positions tell you exactly which uploads exist. Roll back precisely those. Because allSettled has waited for the stragglers, no late write can land after the rollback, which is the failure mode the fail-fast version has.
- Does switching to Promise.allSettled make the failure path slower?Yes, and deliberately so — you now wait for the slowest entry instead of returning at the first rejection. That is the price of knowing the true final state. Where the user should not wait, one common shape is to answer the request from the first failure while a background handler keeps the allSettled promise and reconciles state, accepting that the two paths are now decoupled.
saying these in an interview costs you the question
- Says the remaining promises are cancelled automatically
- Thinks rejecting Promise.all stops the underlying requests
- Assumes later rejections show up as unhandled rejections
- Believes no side effects can land after the await throws
- Claims a promise can be cancelled by dropping its reference