Asynchronous JavaScript — Examples
Example 1 — Three requests awaited one after another
A dashboard loader awaits three independent requests in sequence, and it treats every response as a success. Explain both problems, then make the waits overlap and the failures visible.
async function loadDashboard(id) {
const user = await loadUser(id); // 300ms
const orders = await loadOrders(id); // 400ms
const alerts = await loadAlerts(id); // 200ms
return { user, orders, alerts };
}
// total: ~900ms
const response = await fetch("/api/orders/" + id);
const orders = await response.json(); // a 500 is parsed as if it succeeded
Solution
async function loadOrders(id) {
const response = await fetch("/api/orders/" + id);
if (!response.ok) throw new Error("HTTP " + response.status);
return response.json();
}
async function loadDashboard(id) {
const [user, orders, alerts] = await Promise.all([
loadUser(id), loadOrders(id), loadAlerts(id),
]);
return { user, orders, alerts };
}
// total: ~400ms — the slowest call, not the sum
Awaiting in sequence makes the page wait for the sum of the three durations, while the requests have no dependency on each other at all; Promise.all waits only for the longest and rejects if any of them fails. The second problem is quieter: fetch rejects on a network failure but resolves for a 404 or a 500, so the JSON parse is attempted against an error body and fails with a message that points at the wrong thing. Checking response.ok turns a confusing parse error into a named status.
Example 2 — A search where the slowest response wins
Each keystroke starts a request and the result is rendered whenever it arrives. Typing “keyboard” can end up showing the results for “key”. Say what is racing, then make the abandoned request stop.
async function search(term) {
const response = await fetch("/api/search?q=" + term);
const results = await response.json();
render(results);
}
input.addEventListener("input", () => search(input.value));
// the response for an earlier, shorter term can land last
Solution
let controller = null;
async function search(term) {
if (controller) controller.abort();
controller = new AbortController();
try {
const response = await fetch("/api/search?q=" + term, {
signal: controller.signal,
});
if (!response.ok) throw new Error("HTTP " + response.status);
render(await response.json());
} catch (error) {
if (error.name === "AbortError") return; // superseded, not a failure
showError(error);
}
}
input.addEventListener("input", () => search(input.value));
A promise cannot be cancelled, but the request behind it can, which is what AbortController is for: starting a new search aborts the one in flight, so a stale response can no longer overwrite a fresh one. The abort also arrives as an error, so it has to be distinguished from a real failure or every keystroke would show an error message.