To draw one screen you call three APIs at once with Promise.all. One of them returns a 500, so the code jumps to catch and shows an error message. Yet the server log shows the other two requests were handled to the end. It failed, so why didn't the rest stop? After reading this you will be able to check in the logs what actually stops and what keeps going when Promise.all rejects, and choose whether to add Promise.allSettled or an AbortController for your case.
The example was run on 2026-10-08 on macOS 27.0.1 with Node.js v24.21.0. In Node.js, fetch is handled by the bundled undici 7.29.1. The server is a small fake Node.js API on the same machine (127.0.0.1). /a answers 500 after 100 ms, /b answers 200 after 300 ms and /c after 600 ms, and each finished request is logged. The paths and timings are made up for the explanation.
Promise.all is a way of waiting, not the work itself
First look at when the requests start. The moment ['/a', '/b', '/c'].map((p) => get(p)) runs, fetch is called three times and all three requests are already on the network. Promise.all then takes the three promises that were created and returns a new promise that settles "with an array of results if all succeed, or with the first rejection reason if any fails". It neither starts the requests nor stops them.
JavaScript promises have no cancel operation. A promise only stands for the result of work that is already under way, so when the code waiting for it gives up, the work itself receives no signal. All Promise.all defines is that it rejects as soon as one input rejects; there is no rule about what happens to the other inputs.
DEV NOTES
Promise.all is only a rule for waiting
Case 1 · run with Node.js v24.21.0
1 Three requests start together
Promise.all(['/a','/b','/c'].map(get))- Each fetch starts when map() calls it
- Promise.all only gathers the results
2 /a fails → rejects at once
183 ms rejected: /a HTTP 500- catch gets the first rejection reason
- It does not wait for the others
3 /b and /c run to the end
372 ms /b done · 669 ms /c done- No cancel signal, so they get responses
- The server handled all three
Times are the client.mjs output from 2026-10-08. /a, /b and /c are a fake API on 127.0.0.1 that answers after 100, 300 and 600 ms.
In a real run, requests keep going after catch has finished
The client's get() sends a request with fetch and throws if the response is not ok. fetch does not fail by itself on a 404 or 500, so you have to throw like this for Promise.all to see the failure. On success it prints done (still ran); on failure, stopped and the error name.
The first case is the usual try { await Promise.all(...) } catch. At 183 ms /a got its 500 and failed, and at the same moment Promise.all rejected: /a HTTP 500 was printed. So far, as expected. But afterwards /b received its full response at 372 ms and /c at 669 ms. The server log also shows all three requests as finished. The first request ended at 183 ms rather than about 100 ms, most likely because opening the first connection added time; from the second case on it was around 104 ms.

This becomes a real problem in cases like these. After the error screen is shown, the late result of /b overwrites state and the screen changes again; or the user has moved to another page and the response arrives and touches a component that no longer exists. And if /b was a save rather than a read, the user sees "it failed" while part of the data has been saved.
How to choose: what does one failure mean?
The second case is Promise.allSettled. Even when something fails, it waits until every input has ended and then returns each result as { status: 'fulfilled', value } or { status: 'rejected', reason }. In the run, /a failed at 104 ms, but the results arrived all at once at 607 ms, after /c ended: rejected, fulfilled, fulfilled. For a screen such as a dashboard, where the other cards can still be shown when one fails, this is the right choice.
The third case is when one failure makes the rest pointless. The three requests share the signal of one AbortController, and whichever fails first calls abort().
const ac = new AbortController();
await Promise.all(paths.map((p) =>
get(p, ac.signal).catch((e) => { ac.abort(); throw e; })));
/a failed at 103 ms, and at 112 ms the fetch calls for /b and /c stopped with AbortError. If no reason is passed to abort(), the reason is a DOMException named AbortError. The two stopped requests call abort() again in their own catch, but the DOM Standard makes aborting an already aborted signal return without doing anything. In a separate check, calling abort() twice fired the abort event only once. The rejections of /b and /c after Promise.all had already rejected did not produce an unhandled rejection warning either.
DEV NOTES
Decide first what one failure should mean
Same three requests, three ways to combine them
Promise.all
183 ms rejected · /b·/c still run- catch runs at the first failure
- Other requests quietly keep going
Promise.allSettled
607 ms rejected, fulfilled, fulfilled- Waits until every one ends
- Check each success or failure
all + AbortController
112 ms /b·/c AbortError- abort() at the first failure
- Other fetches stop with AbortError
Values come from the same run on 2026-10-08. If partial results are still useful, use allSettled; if one failure makes the rest pointless, use Promise.all with abort.

abort() does not undo what the server has done
In the third case the server log shows server saw client close ... (work stopped). That happens only because the lab server was written that way. It listens for the response's close event and, if the connection closes before the response is sent, cancels the scheduled work with clearTimeout. What abort() does ends at making the client stop waiting and closing the connection. If the server does not listen for that, has already finished writing to the database, or has already asked another service to take a payment, that work stays done.
So when you combine requests that cannot be undone, such as saves or payments, do not treat abort() as a cancel button. Merge them into a single API call that handles them together, or design on the server what to undo when something fails. abort() is the right tool for cutting waste on requests whose result you no longer need, such as reads.
DEV NOTES
What abort() stops, and what it cannot
From the client to the server
1 Client: stops waiting
ac.abort() → AbortError- fetch rejects with AbortError
- Response handling never runs
2 Connection: closed
res.on('close') → clearTimeout- The server can see the connection end
- Its own code decides whether to stop
3 Work already done: stays
abort ≠ rollback- A finished write or payment is not undone
- Undo has to be designed separately
The lab server was written to stop work when the connection closes. That does not mean every server behaves this way.
The takeaway
When Promise.all rejects, it only means "I will not wait any longer"; it does not mean the requests already sent were stopped. When you see Promise.all in your code, ask one question: if one fails, will I still use the other results? If yes, switch to Promise.allSettled. If no, pass the same AbortController signal to each request and call abort() on the first failure. And if requests that cannot be undone, such as saves, are in the mix, the work the server has already done needs separate handling either way.
Add your perspective.
Share a question, another approach, or something you have tried.
Checking sign-in…
Loading comments…