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.
| Pattern | Style | Error handling | Main drawback |
|---|---|---|---|
| Callbacks | Function passed to be called later | Error-first arguments, checked manually | Deep nesting and inverted control |
| Promise chains | then and catch methods | One catch for the chain | Branching logic becomes verbose |
| Async/await | Sequential code with await | Standard try, catch and finally | Easy to serialize work accidentally |
| Async iteration | for await over async iterables | try and catch around the loop | Processes items one at a time |
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.
| Combinator | Settles when | Rejection behavior | Typical use |
|---|---|---|---|
| Promise.all | All promises fulfill | Rejects on the first failure | Load several required resources |
| Promise.allSettled | All promises settle | Never rejects; reports each outcome | Independent tasks where partial success is fine |
| Promise.race | The first promise settles | Follows the first settled result | Timeout against a slow operation |
| Promise.any | The first promise fulfills | Rejects only if all fail | Fastest of several equivalent sources |
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.
| Symptom | Cause | Fix |
|---|---|---|
| Page slower than expected | Independent awaits run sequentially | Start operations first, then await Promise.all |
| Function returns a pending promise | Caller forgot to await the result | Await the call or handle it with then |
| Loop finishes before work completes | await used inside forEach | Use for…of or Promise.all with map |
| Silent failures | Missing try and catch, unhandled rejection | Catch at the right level and report errors |
| Interface freezes despite async code | Heavy synchronous computation in the function | Move computation to a web worker |
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.