Lesson 24 of 30
Selecting 10 items and hitting delete doesn't fire one request — it fires 10. Treating that as a single success-or-failure outcome is a real, common bug, because some of the 10 will succeed and some won't.
12 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
A user selects 10 items and clicks bulk-delete, which fires 10 independent DELETE requests. 8 succeed and 2 fail. What should the UI do?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Some UI actions look like one action but are actually many independent requests fired together: bulk-deleting a set of selected items, submitting several related updates at once. A common mistake is writing that as if it were a single atomic operation — one loading state, one "success" or "failure" message at the end. In reality, each request in the batch succeeds or fails on its own. A permission issue might block one specific delete; a transient error might hit a different one; the rest go through fine. Reporting that as one flat outcome is actively misleading either way: "success" hides the items that actually failed and still need attention, and "failure" hides the items that actually went through, which can lead a user to retry an action that already partly happened.
The honest version of this UI has to know, precisely, which items succeeded and which failed, and say so.
The tool for firing independent requests and getting every individual result, regardless of whether some of them failed, is Promise.allSettled. It's a deliberate contrast with Promise.all, which rejects the moment any single promise in the batch rejects — exactly the wrong behavior here, since one failed delete shouldn't prevent finding out whether the other nine succeeded.
type DeleteResult = { id: string; ok: true } | { id: string; ok: false; reason: string };
async function deleteItem(id: string): Promise<DeleteResult> {
const res = await fetch(`/api/items/${id}`, { method: "DELETE" });
if (res.ok) return { id, ok: true };
const body = await res.json().catch(() => ({}));
return { id, ok: false, reason: body.message ?? `Request failed (${res.status})` };
}
async function deleteSelected(ids: string[]) {
const settled = await Promise.allSettled(ids.map(deleteItem));
// Every entry resolves (deleteItem never throws), so each is a "fulfilled"
// settlement wrapping either an ok or a failed DeleteResult.
const results = settled.map((entry) =>
entry.status === "fulfilled" ? entry.value : { id: "unknown", ok: false as const, reason: "Unexpected error" },
);
const succeeded = results.filter((r) => r.ok);
const failed = results.filter((r) => !r.ok);
return { succeeded, failed };
}
const { succeeded, failed } = await deleteSelected(selectedIds);
if (failed.length === 0) {
showToast(`${succeeded.length} deleted.`);
} else if (succeeded.length === 0) {
showToast(`Delete failed for all ${failed.length} items.`);
} else {
showToast(
`${succeeded.length} of ${succeeded.length + failed.length} deleted. ` +
`${failed.length} failed: ${failed.map((f) => f.reason).join(", ")}`,
);
}
// Remove only the successfully deleted ids from local UI state — the
// failed ones stay visible and selected, so the user can see exactly
// what still needs attention (and retry just those).
removeFromList(succeeded.map((r) => r.id));
Promise.all is the wrong tool for an intentionally-independent batch. It rejects as soon as any one promise rejects, discarding the results of everything else that may have already succeeded — exactly backwards for a batch where partial success is a real, expected outcome.deleteItem does above, is what makes Promise.allSettled actually useful here."What happens if 2 out of 10 items in a bulk action fail?" is a real, specific probe for whether a candidate defaults to atomic all-or-nothing thinking or actually reasons about a batch of independent requests — naming Promise.allSettled and itemized feedback by name is the concrete signal of the latter.