JavaScript Async

Published Updated

This topic covers the JavaScript that runs later. Requests that take time, results that arrive out of order, and the syntax that lets you write all of it without nesting callbacks five levels deep.

It is also where most half-finished projects stall. A form that submits twice, a search box that shows the wrong results, a spinner that never stops: all three can be async bugs, and none of them look like async bugs from the outside. Synchronous event and state logic can produce the same three symptoms.

JavaScript Async Topics

  • Promises and async/await covers what a promise actually is, the then and catch pair, the async and await keywords that usually stand in for a then chain in everyday code, overlapping work with Promise.all, and where a thrown error ends up.
  • Fetch, JSON, and the Network covers requesting data over HTTP, the response.ok check that almost every tutorial skips, turning a response into an object, sending data with POST, and cancelling a request that is taking too long.
  • Timers, Ordering, and Race Conditions covers setTimeout and setInterval, the order things actually print, the stale response problem that breaks search boxes, and the debounce and throttle patterns that stop a handler firing on every keystroke.

The Rule Everything Here Depends On

One piece of JavaScript runs at a time, and it runs to the end before anything else gets a turn. When something slow starts, the API you called usually hands back a placeholder, your code carries on, and the engine returns to your callback once there is a result.

Not every API is shaped that way. Some return a promise, some take a callback, and some hand back a handle or nothing at all. The pattern below is the one they all share.

console.log("1: asking for the data");

setTimeout(() => console.log("3: the data arrived"), 0);

console.log("2: carrying on");
// node output:
// 1: asking for the data
// 2: carrying on
// 3: the data arrived

The delay was zero and the third line still printed last. That is not a timing accident. Work handed off this way is queued, and the queue is only read once the currently running piece of code has finished and given the engine back.

Every subject in this topic follows from that one behaviour. Promises exist to give you a handle on a value that has not arrived. Race conditions exist because two of those handles can come back in an order you did not choose.

How the Three Guides Fit Together

The first guide is the vocabulary. A promise, the three states it can be in, and the await keyword that lets you write waiting code in a straight line. Read it first, because the other two use await in nearly every example without pausing to explain it.

The second guide is the reason most readers came. Fetching data from an API is one of the most common async jobs in a real project, and it is also where the failure handling is easiest to get wrong.

The third guide is what happens once you have more than one of those calls in flight. Ordering, cancellation, and the small set of patterns that keep a fast typist from seeing results for a search they already changed.

Two neighbouring subjects sit outside this topic. Attaching a handler to a button or a form belongs to the DOM rather than to async work, and the language basics such as functions and closures are covered in JavaScript Language Basics.

Four Requests, One Wait

The most expensive async mistake in everyday code is not a syntax error. It is waiting for four things one after another when nothing required you to.

// urls holds four endpoints; getJson wraps fetch and returns
// the parsed body. Each request takes roughly 300ms.

// sequential: each await holds the next request back
const sequential = [];
for (const url of urls) {
  sequential.push(await getJson(url));
}

// overlapping: all four start, then you wait once
const overlapping = await Promise.all(urls.map(getJson));
// node output, timed:
// sequential:  1218ms
// overlapping:  312ms

When all four requests fulfill, both produce the same values. The loop asks for the second URL only once the first has come back, so its total is roughly the four waits added together.

The second version splits the job in two. urls.map(getJson) starts all four requests, and Promise.all watches those promises and waits for them, so the total is roughly the slowest single request.

Choosing between them is a question about dependency rather than speed. If the second request needs a value from the first, the loop is correct and the delay is real work. If the four calls know nothing about each other, the loop is only spending time.

One detail worth carrying into the guides: Promise.all rejects as soon as any input rejects, and the requests already in flight are not cancelled by that. When you want every outcome regardless of failures, Promise.allSettled waits for all of them and reports each result separately.

The promises guide works through both shapes with error handling attached, and the timers and races guide covers what happens when results come back in an order you did not plan for.

Telling an Async Bug from an Ordinary One

The three symptoms named at the top of this page are the ones people misdiagnose most often. Three questions gather clues faster than reading the code again. None of them settles the question on its own.

First, does the wrong behaviour survive when the slow part is removed? Swap the request for a value returned immediately, and a bug that stays put suggests the event handling or the state rather than the waiting.

Second, does the output order change between runs? Varying output can indicate an ordering problem, though plenty of async bugs are perfectly repeatable and plenty of ordinary ones are not.

Third, does it only show up on a slow connection or a tired machine? Throttling the network in your browser tools may expose a race that a fast local server was hiding.

Treat all three as evidence rather than a verdict, because the fixes have nothing in common. A missing await, a missing response.ok check, and a stale response are separate problems wearing the same symptom.

Common Pitfalls

  • Treating a failed request as an error: fetch resolves happily on a 404 or a 500, so code without an response.ok check reads a server error page as data. The fetch guide covers the two failure families and how to tell them apart.
  • Forgetting await and wrapping the call in try: the function returns before anything fails, the catch block never runs, and the failure surfaces later, somewhere you were not looking. What the host does with it then varies. The promises guide shows exactly what that looks like.
  • Awaiting inside a loop when the calls do not depend on each other: four independent requests take roughly the sum of their durations rather than the longest of them. Promise.all is the fix, and the same guide covers when sequential really is correct.
  • Rendering whatever comes back last: a slow earlier request can land after a fast later one and overwrite it. The timers and races guide covers the two guards worth knowing.
  • Leaving an interval running: setInterval keeps firing until something calls clearInterval, whether or not the thing that started it is still on screen. The same guide covers cleanup.

Common Questions

Which guide should you read first?

Start with promises and async/await, because the other two assume you can read an async function without stopping. Fetch comes next, since a network call is where most people meet a promise for the first time. Timers and races come last, because they are about what happens when two of those calls overlap.

Why does async code feel harder than the rest of JavaScript?

Because the line order on screen stops matching the order things happen. Synchronous statements run in order and finish before anything else gets a turn, and async work does not: a function can return before its result exists. These three guides are mostly about learning to read that gap rather than fight it.

Does async make code run in parallel?

Not in the sense of two pieces of your code executing at the same moment. What it buys is the ability to start slow work, hand control back, and pick the result up later. Four network requests can be in flight at once, and it is the waiting that overlaps rather than your JavaScript.

Do you still need callbacks if you have await?

Yes, and not as a fallback. Event listeners, setTimeout, and array methods such as map all take callbacks, and none of them are going away. What await usually replaces is an explicit then chain, and try/catch is what replaces catch. A callback-only API still needs a callback, or a wrapper that turns it into a promise.

Should every async function have a try/catch?

No, and wrapping every one of them tends to hide failures rather than handle them. A catch belongs where the code can actually do something useful, which is often further up the call chain. What matters is that somebody handles it, because a rejection nothing catches is easy to lose.

Where do you run the examples on these pages?

Node runs almost all of them, including the fetch examples, since fetch is built into current Node. Save a file such as app.js next to a package.json containing type module, then run node app.js. The few examples that need a real web page are marked as browser-side where they appear.

Continue Learning JavaScript

  • Return to the JavaScript guide for where the language fits, the full learning path, and what to build first.
  • Read JavaScript Language Basics for the values, functions, and closures every example here assumes.
  • Read TypeScript Types when you want a compiler to tell you what shape an API response actually has before you use it.

Sources

  1. [1]
    Using Promises
    (developer.mozilla.org)
  2. [2]
    Asynchronous JavaScript
    (developer.mozilla.org)
  3. [3]
    Fetch API
    (developer.mozilla.org)