Node.js · Recorded experiment

Does Promise.all Cancel Other Promises on Rejection?

No automatic cancellation: a controlled Node.js file-write experiment shows two tasks continuing after Promise.all rejects, then compares cooperative abort checks.

Recorded: (UTC) · Node v22.22.3 · darwin 27.0.1 / arm64

Read this article in: 简体中文

When one input rejects, Promise.all rejects its combined promise. Other work that has already started can continue. If that work writes files, entering catch does not mean the directory has stopped changing.

This experiment starts three tasks. The first fails immediately. The other two wait at a gate before writing a.txt and b.txt. The directory is empty when catch runs. After the gate opens and all tasks settle, both files exist. A second scenario adds an abort check before writing and produces no files.

Two recorded runs

The same script in two independent runs
ScenarioRun 1Run 2
Catch the combined rejectionFiles at catch: 0; after settlement: 2; rejected / fulfilled / fulfilledFiles at catch: 0; after settlement: 2; rejected / fulfilled / fulfilled
Abort in catch; check before writingFiles at catch: 0; after settlement: 0; rejected / rejected / rejectedFiles at catch: 0; after settlement: 0; rejected / rejected / rejected
Abort after a file was writtenFiles remaining after abort: 1Files remaining after abort: 1

The async calls have already started

Calling an async function starts its execution. In this script, the array contains promises returned by those calls. Promise.all observes their results; it does not add a cancellation check inside the two writer functions.

The gate makes the order explicit. The script waits for the combined rejection, inspects the empty directory, opens the gate and then awaits Promise.allSettled. This avoids depending on a short timer to make the failure happen before the writes. The following outline uses abbreviated function names; the download contains the full implementation.

const tasks = [failImmediately(), writeAfterGate('a.txt'), writeAfterGate('b.txt')];
try {
  await Promise.all(tasks);
} catch (error) {
  // Only the combined promise is known to have rejected here.
}
releaseGate();
await Promise.allSettled(tasks);

Place the cancellation check before the side effect

In the cooperative scenario, catch calls controller.abort(). Once the gate opens, each writer calls signal.throwIfAborted() before writeFile. Both writers reject without writing, so all three tasks end in the rejected state.

The order matters. In this experiment, the abort signal is set before either writer checks it. A controller does not know how an arbitrary promise works. The task must check the signal itself, or pass it to an operation that supports cancellation. This check does not establish that every possible file-write race is handled.

await gate.promise;
controller.signal.throwIfAborted();
await writeFile(target, 'written after aggregate rejection');

An abort signal does not roll back a completed write

The third case writes already.txt and then calls abort. The file remains in the directory. Nothing in the abort call deletes the file or restores the previous contents.

If a batch must be delivered only when all work succeeds, a temporary staging directory can separate intermediate output from the final destination. Decide whether to deliver or clean it after all tasks settle. That is a design option to investigate, not a transaction guarantee established by this script. Writes across systems need their own idempotency or compensation rules.

Wait for cleanup when later code depends on the result

Code that inspects the output directory immediately after catch may race with tasks still running. The experiment uses allSettled to wait for every task before its final inspection. allSettled collects outcomes; it does not cancel tasks either.

To inspect the mechanism, remove the signal check from the cooperative writer and rerun it. Files can then be created after the gate opens, and the assertion expecting an empty directory will fail. Keep a separate output record for your modified version rather than overwriting the published record.

Run the experiment on your machine

Use Node.js 22 or later. No dependencies need to be installed. The script uses Node built-in modules, runs two rounds and checks assertions. Choose a new record path so you do not overwrite an earlier result.

node --version
node run.mjs
node run.mjs --record my-results.json

Both rounds use self-authored tasks and fresh temporary directories, which the script removes afterward. Gates determine ordering. The experiment does not measure disk performance, cancel a remote request or test database rollback. The gap between a cancellation check and a real side effect requires attention in the operation you use.

Both rounds were run by the same operator on one machine. No independent reader rerun has been recorded. Save your own output when trying another environment; a failed assertion stops the script.

Check the script SHA-256

571f4777b8e9bf25e2f7277e73e5659886e2004e5f37c50312ad0eac585e5bab

This digest identifies the downloadable script. Full observations are in verification.json.