W3docs

JavaScript async/await

Learn JavaScript async/await: how async functions return promises, how await works, try/catch error handling, and sequential vs parallel execution.

The async/await syntax is the modern, readable way to work with asynchronous code in JavaScript. It is built directly on top of promisesasync/await does not replace them, it gives you cleaner syntax for consuming them. Instead of chaining .then() callbacks, you write code that reads top-to-bottom like ordinary synchronous code, while the engine handles the waiting behind the scenes.

This chapter covers what async functions return, how await pauses execution, error handling with try...catch, running tasks sequentially versus in parallel, top-level await in modules, and the pitfalls that trip up most developers.

async functions always return a promise

Marking a function async does two things: it lets you use await inside the function body, and it guarantees the function returns a promise. Whatever value you return becomes the resolved value of that promise; if you throw, the promise rejects.

javascript— editable

Because the return value is a promise, the caller must still await it (or use .then()) to read the actual value. A common beginner mistake is expecting greet() to give back 'Hello' directly.

await pauses until the promise settles

The await operator can only be used inside an async function (or at the top level of a module — see below). It pauses the function until the promise to its right settles: on fulfillment it returns the resolved value, on rejection it throws the rejection reason.

Crucially, await does not block the whole program. It only suspends the current async function; the rest of your code and the event loop keep running.

javascript— editable

You can await any value, not just a promise. Non-promise values are wrapped and resolved immediately, so await 5 simply yields 5.

Error handling with try...catch

One of the biggest wins of async/await is that you handle errors with the same try...catch you already use for synchronous code. A rejected promise turns into a thrown exception at the await point, which catch can intercept.

javascript— editable

If you do not catch a rejection, it becomes an unhandled promise rejection. For a deeper look at the promise-side mechanics, see error handling with promises.

A real-world example: fetching data and reporting failures cleanly.

javascript— editable

Note the explicit response.ok check: fetch only rejects on network failure, not on HTTP error statuses like 404 or 500, so you must inspect the response yourself.

Sequential vs. parallel execution

This is where await is most often misused. When you await operations one after another, they run sequentially — each waits for the previous to finish. That is correct only when a later task depends on an earlier result.

Sequential (when tasks depend on each other)

async function pipeline() {
  const user = await getUser(1);          // step 1
  const posts = await getPosts(user.id);  // needs user.id, so must wait
  return posts;
}

Parallel (when tasks are independent)

If the tasks do not depend on each other, awaiting them in sequence wastes time. Start them all, then await together with Promise.all:

javascript— editable

Promise.all rejects as soon as any one of its promises rejects. If you instead want to wait for every promise regardless of success or failure, use Promise.allSettled. See the promise API for the full family of combinators.

Top-level await in modules

Inside ES modules (<script type="module"> or .mjs files), you can use await at the top level without wrapping it in an async function:

// data.mjs — an ES module
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const todo = await response.json();

export { todo };

This is handy for module initialization, such as loading config before the module's exports are ready. Note that a module using top-level await delays the evaluation of any module that imports it. Top-level await works only in modules — using it in a classic script or a regular function is a syntax error.

Common pitfalls

Don't await inside a loop for independent work

Using await inside a for loop forces iterations to run one at a time. If the iterations are independent, this is needlessly slow.

javascript— editable

Reach for await in a loop only when each iteration genuinely depends on the previous one, or when you must throttle requests.

Other gotchas

  • forEach ignores async callbacks. array.forEach(async ...) does not wait for the promises. Use a for...of loop or Promise.all(array.map(...)) instead.
  • Don't forget await. Calling an async function without await (or .then()) returns a pending promise and silently swallows errors. Linters often flag "floating" promises.
  • fetch doesn't reject on HTTP errors. Always check response.ok, as shown above.

Conclusion

async/await makes asynchronous JavaScript read like synchronous code while keeping the event loop free. Remember the essentials: every async function returns a promise, await pauses only the current function, errors flow through try...catch, and you should run independent tasks in parallel with Promise.all rather than awaiting them one by one. To strengthen the foundation underneath this syntax, review promises and promise chaining.

Practice

Practice
What is the function of the 'async' keyword in JavaScript?
What is the function of the 'async' keyword in JavaScript?
Was this page helpful?