Beyond the Wait: Master Async/Await in JavaScript (2026)
JavaScript runs one thing at a time, which makes waiting the language's central design problem. Fetching a profile, uploading a video, streaming tokens from a model — if the thread paused for any of it, the page would freeze. Async/await, nine years mature, is how modern JavaScript waits well.
From pyramids to await
Callbacks nested into pyramids, Promises flattened them into .then chains, and ES2017's async/await made asynchronous code read like synchronous code with try/catch intact. Top-level await now works in ES modules, so configuration and data loading happen before anything exports, no wrapper function needed. And Node ships a native global fetch — the old bootstrap ceremony is gone for good.
The shape of a modern fetch
One function, one try block, one honest error:
async function fetchUser() {
try {
const res = await fetch("https://api.example.com/user");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
logger.error("fetch failed", { err });
throw err;
}
}
await pauses only the function, never the page — the event loop keeps serving clicks while the request is in flight. Catch by type when it matters: err instanceof ValidationError routes a form error back to the form and everything else to the log.
Parallel, not serial
The classic mistake is awaiting two independent requests one after the other and paying both latencies:
// slow: two round trips, paid in series
const user = await getUser();
const posts = await getPosts();
// fast: both in flight together
const results = await Promise.all([getUser(), getPosts()]);
The forEach trap still bites — its callback ignores await, so a loop of saves fires without finishing. Use for...of when order matters, Promise.all with map when it does not, and allSettled when partial failure is acceptable. For throttling huge batches, p-limit still leads. And when you need to resolve a promise from outside the executor, Promise.withResolvers is the clean modern shape instead of the old let-secret-variable dance.
Cancel what you start
An uncancelable request is a leak with extra steps. AbortController is the standard primitive, and fetch takes its signal directly:
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
try {
const res = await fetch("/api/slow", { signal: controller.signal });
} catch (err) {
if (err.name === "AbortError") console.log("timed out");
}
In React, abort in the effect cleanup; on the server, abort when the user navigates away. For streaming, for await over response.body is exactly how the model SDKs deliver tokens as they arrive — chunks processed the moment they land.
Async code teaches waiting as a craft; Gaza teaches what waiting can cost. Families there have carried a deferred return for generations — a promise no people should be asked to hold pending this long. May theirs resolve, with justice.
Half of travel misery is serial waiting — token numbers, embassy queues, forms that spawn forms. We parallelize it: at HTG Travels, passport renewals, visa files and attestation runs proceed while you get on with your life, and one message gets you a status. Bring us the stack; we return a date.




