How do you send multiple API requests on a single button click
Fire all with `Promise.all` if independent and you need all results; `Promise.allSettled` if you want every result regardless of failure; `p-limit` or a small async-pool for many requests (avoid hammering the server); chain awaits only when one depends on another's output. Add AbortController for cancel-on-unmount. Disable the button while in flight.
Two requests, both must succeed
async function onClick() {
setLoading(true);
try {
const [a, b] = await Promise.all([fetch("/a"), fetch("/b")]);
use(a, b);
} catch (err) {
showError(err);
} finally {
setLoading(false);
}
}Both fire in parallel; one round-trip total. Promise.all rejects on the first failure.
Both can fail independently
const results = await Promise.allSettled([fetch("/a"), fetch("/b")]);
for (const r of results) {
if (r.status === "fulfilled") use(r.value);
else log(r.reason);
}Good for dashboards — partial success is meaningful.
Sequential when one depends on the other
const user = await fetch("/user").then(r => r.json());
const posts = await fetch(`/posts?user=${user.id}`).then(r => r.json());Many requests — limit concurrency
Don't fire 100 at once; servers and connection pools both suffer:
import pLimit from "p-limit";
const limit = pLimit(5);
const results = await Promise.all(items.map((i) => limit(() => fetch(`/api/${i}`))));Or a tiny pool:
async function asyncPool(limit, items, iter) {
const ret = []; const executing = new Set();
for (const item of items) {
const p = Promise.resolve().then(() => iter(item));
ret.push(p);
executing.add(p);
p.finally(() => executing.delete(p));
if (executing.size >= limit) await Promise.race(executing);
}
return Promise.all(ret);
}UX: disable + show progress
<button onClick={onClick} disabled={loading}>{loading ? "Saving…" : "Save"}</button>Or aria-busy on the surrounding region.
Cancellation on unmount or re-click
const ac = new AbortController();
fetch("/a", { signal: ac.signal });
// in cleanup
ac.abort();Critical in React effects so a stale response doesn't update state after the component unmounted.
Optimistic UI
For "save" buttons, render the change immediately, rollback on failure. See [[design-the-send-message-feature-like-slack-ui-client-logic]].
Common mistakes
await-ing sequentially when calls are independent.- Firing the same request twice on double-click (no disabling).
- Not catching rejection → unhandled promise rejection.
- No cancellation → stale response races.
- Hammering server with unbounded concurrency.
Interview framing
"For two independent calls, Promise.all. If partial failure is acceptable, Promise.allSettled. Sequential awaits only when later calls depend on earlier values. For many calls, cap concurrency with p-limit or a tiny async pool — usually 5–10 parallel is the sweet spot. Always disable the button while in flight, show progress, cancel on unmount via AbortController. The classic bug is sequential awaits for independent work — instant 2x latency."
Follow-up questions
- •How would you handle limited concurrency?
- •Why use AbortController?
- •How would you debounce a button to avoid double-fire?
Common mistakes
- •Sequential awaits for independent calls.
- •No disabled state — double submit.
- •No cancellation on unmount.
- •Unhandled rejection.
Performance considerations
- •Parallelizing independent work saves N-1 RTTs. Concurrency limit prevents server flood.
Edge cases
- •User clicks again mid-flight.
- •One call is faster — should we render partially?
- •Network drops mid-request.
Real-world examples
- •Save buttons that hit multiple endpoints, dashboard refresh, batch save.