JavaScript Async/Await

async/await is a syntax for working with Promises. It lets you write asynchronous code that reads like synchronous code, which makes it easier to follow. Under the hood, it is still Promises.

The async keyword

Add async in front of a function declaration or expression to make it async. An async function always returns a Promise, even if you return a plain value.

async function greet() {
  return 'Hello';
}

greet().then((message) => console.log(message)); // Hello

You can also write async arrow functions:

const greet = async () => {
  return 'Hello';
};

The await keyword

await pauses the async function until a Promise settles and then returns the resolved value. You can only use await inside an async function (or at the top level of an ES module).

async function getUsers() {
  const response = await fetch('https://api.github.com/users');
  const users = await response.json();
  console.log(users);
}

getUsers();

Compare this to the same code written with .then():

// Using Promises
function getUsers() {
  return fetch('https://api.github.com/users')
    .then((response) => response.json())
    .then((users) => console.log(users));
}

Both do the same thing. async/await is usually easier to read, especially when there are several steps.

Handling errors

Use try/catch to catch errors in async functions.

async function getUsers() {
  try {
    const response = await fetch('https://api.github.com/users');
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    const users = await response.json();
    return users;
  } catch (err) {
    console.error('Failed to load users:', err.message);
  }
}

The catch block catches both network errors (no connection) and errors you throw manually (like the !response.ok check above).

Running multiple operations in parallel

Awaiting Promises one after another runs them sequentially. If the operations are independent, that is slower than it needs to be.

// Slow: waits for the first before starting the second
async function loadData() {
  const users = await fetch('/api/users').then((r) => r.json());
  const posts = await fetch('/api/posts').then((r) => r.json());
  return { users, posts };
}

Start both at the same time, then await both together:

// Fast: both requests start at the same time
async function loadData() {
  const [users, posts] = await Promise.all([
    fetch('/api/users').then((r) => r.json()),
    fetch('/api/posts').then((r) => r.json()),
  ]);
  return { users, posts };
}

Top-level await

In ES modules (files with type="module" or .mjs extension), you can use await at the top level without an async function wrapper.

// app.mjs or app.js with "type": "module" in package.json
const response = await fetch('https://api.github.com/users');
const users = await response.json();
console.log(users);

This only works in modules, not in regular scripts.

Common mistakes

Awaiting inside a loop instead of using Promise.all

// Slow: runs requests one at a time
async function loadUsers(ids) {
  const users = [];
  for (const id of ids) {
    const user = await fetchUser(id); // waits for each one in sequence
    users.push(user);
  }
  return users;
}

// Fast: runs all requests in parallel
async function loadUsers(ids) {
  return Promise.all(ids.map((id) => fetchUser(id)));
}

Forgetting to handle errors

If an awaited Promise rejects and you have no try/catch, you get an unhandled Promise rejection. Wrap your async logic in try/catch or attach a .catch() to the call site.

Expecting async functions to return plain values

async function getName() {
  return 'Anna';
}

const name = getName();
console.log(name); // Promise { 'Anna' }, not 'Anna'

// You need to await it
const name = await getName();
console.log(name); // Anna
  • Promises : the foundation that async/await is built on
  • Fetch API : the most common use of async/await in practice