Async/await

On this page 7

Async/await is JavaScript syntax that lets developers write promise-based asynchronous code in a sequential, readable style. An async function always returns a promise, and inside it the await keyword pauses that function until a promise settles, then resumes with the fulfilled value or throws the rejection reason as an exception.

In browser JavaScript, async/await is the standard way to handle network requests, timers, file reads and other operations that finish later. It was introduced in ECMAScript 2017 and is syntactic sugar over promises: it does not create threads or make code run in parallel. While a function waits, the main thread stays free to render the page and respond to user input.

How async functions and await work

Declaring a function with async changes two things. Its return value is automatically wrapped in a promise, and an exception thrown inside it becomes a rejected promise instead of propagating synchronously to the caller.

When execution reaches await, the engine evaluates the expression, converts it to a promise if needed and suspends the async function. Control returns to the caller, and the rest of the call stack continues. When the promise settles, the continuation of the function is queued as a microtask and resumes in the next microtask checkpoint, after the current synchronous code finishes.

This explains a frequently surprising ordering. In the sequence console.log(1), a call to an async function that logs 2 after await null, then console.log(3), the output is 1, 3, 2. Even a value that is already available causes a pause until the current synchronous task completes.

Top-level await is allowed in JavaScript modules, such as scripts loaded with type="module", but not in classic scripts. A module using top-level await delays the evaluation of modules that import it until the awaited promise settles.

Async/await compared with callbacks and promise chains

Async/await is the third generation of asynchronous patterns in JavaScript. Each generation solved readability and error handling problems of the previous one while keeping the same underlying event loop.

PatternStyleError handlingMain drawback
CallbacksFunction passed to be called laterError-first arguments, checked manuallyDeep nesting and inverted control
Promise chainsthen and catch methodsOne catch for the chainBranching logic becomes verbose
Async/awaitSequential code with awaitStandard try, catch and finallyEasy to serialize work accidentally
Async iterationfor await over async iterablestry and catch around the loopProcesses items one at a time
Asynchronous patterns in JavaScript

The principal gain is unified control flow. Loops, conditionals and try/catch blocks work across asynchronous steps exactly as they do in synchronous code, and stack traces in modern engines include async call frames, which makes debugging considerably easier than with nested callbacks.

Sequential and concurrent execution

The most common performance mistake with async/await is awaiting independent operations one after another. Two requests that each take 300 milliseconds take about 600 milliseconds when awaited sequentially, but about 300 milliseconds when started together and awaited as a group.

Promise combinators handle concurrency. The table below compares them and shows when each fits.

CombinatorSettles whenRejection behaviorTypical use
Promise.allAll promises fulfillRejects on the first failureLoad several required resources
Promise.allSettledAll promises settleNever rejects; reports each outcomeIndependent tasks where partial success is fine
Promise.raceThe first promise settlesFollows the first settled resultTimeout against a slow operation
Promise.anyThe first promise fulfillsRejects only if all failFastest of several equivalent sources
Promise combinators used with await

In loops, await inside forEach does not wait at all, because forEach ignores the promises its callback returns. A for...of loop runs iterations sequentially, while mapping items to promises and passing them to Promise.all runs them concurrently. For large lists, a concurrency limit, for example five requests at a time, avoids overwhelming the server.

Error handling and cancellation

Errors in async functions are handled with try, catch and finally, wrapped around one or several await expressions. A rejection that is never awaited or caught triggers the browser’s unhandledrejection event and a console error.

A detail that matters with the Fetch API is that fetch rejects only on network failure, not on HTTP error statuses. A response with status 404 or 500 still fulfills, so code must check response.ok and throw explicitly when the status indicates failure.

Promises cannot be canceled by themselves. In browser JavaScript, cancellation uses an AbortController: its signal is passed to fetch or other APIs, and calling abort() rejects the pending promise with an AbortError. Search fields commonly abort the previous request when the user types a new character, which prevents stale results from overwriting newer ones.

Common mistakes and limitations

Async/await makes asynchronous code look synchronous, and that resemblance is the source of most mistakes. The table summarizes typical symptoms, causes and fixes.

SymptomCauseFix
Page slower than expectedIndependent awaits run sequentiallyStart operations first, then await Promise.all
Function returns a pending promiseCaller forgot to await the resultAwait the call or handle it with then
Loop finishes before work completesawait used inside forEachUse for…of or Promise.all with map
Silent failuresMissing try and catch, unhandled rejectionCatch at the right level and report errors
Interface freezes despite async codeHeavy synchronous computation in the functionMove computation to a web worker
Async/await mistakes and remedies

The last row reflects a fundamental limitation: async/await does not make CPU-intensive work non-blocking. Code between awaits still runs on the main thread, so long calculations belong in a web worker, which provides real parallelism.

Put simply

Some tasks on a web page take time, such as loading data from a server. Async/await lets a programmer write wait for this, then continue in a natural order. Meanwhile, the page keeps working.

It is like ordering at a cafe and receiving a buzzer. You do not stand frozen at the counter. You sit down, and when the buzzer rings, you pick up your order. The page stays responsive while it waits.

In practice

A developer builds a dashboard that loads a user profile, recent orders and notifications from three separate endpoints. The first version awaits each request in turn, and the dashboard takes about 900 milliseconds to appear on a typical connection.

The developer starts all three requests at once and awaits them with Promise.allSettled, so a failing notifications service no longer blocks the profile and orders. Each response is checked with response.ok, and an AbortController cancels the requests when the user navigates away. The dashboard now appears in roughly the time of the slowest request, and partial failures show a small message instead of an empty page.