Overview

async/await is the modern way to handle asynchronous operations in JavaScript. It builds on Promises but makes asynchronous code read like sequential code. This tutorial covers the syntax, error handling, and the concurrency patterns that matter in production.

From Callbacks to Promises to async/await

EraStyleProblem
Callbacksfs.readFile(path, cb)Callback hell with nested error handling
Promisesfetch(url).then(...)Still chains; error handling scattered
async/awaitconst res = await fetch(url)Reads like synchronous code

Basic Syntax

async function getUser(id) {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}

getUser(1).then(user => console.log(user));

An async function always returns a Promise. await pauses execution until the Promise settles, then returns the resolved value.

Error Handling

async function loadProfile(id) {
  try {
    const user = await getUser(id);
    const posts = await getPosts(user.id);
    return { user, posts };
  } catch (err) {
    console.error("Failed to load profile:", err.message);
    throw err;
  } finally {
    console.log("Request finished");
  }
}

try/catch/finally works with await exactly as it does with synchronous code.

Sequential vs Parallel Execution

// Sequential — total time = t1 + t2 + t3
const user = await getUser(1);
const posts = await getPosts(1);
const comments = await getComments(1);

// Parallel — total time = max(t1, t2, t3)
const [user, posts, comments] = await Promise.all([
  getUser(1),
  getPosts(1),
  getComments(1)
]);

Use Promise.all whenever the requests do not depend on each other.

Promise Combinators

MethodBehavior
Promise.allWaits for all; rejects on first failure
Promise.allSettledWaits for all; returns status for each
Promise.raceSettles with the first completed promise
Promise.anyResolves with the first fulfilled promise

Using async/await in Loops

// Sequential — correct when order matters
for (const id of ids) {
  const user = await getUser(id);
  results.push(user);
}

// Parallel with concurrency limit
async function mapLimit(items, limit, fn) {
  const results = [];
  const executing = [];

  for (const item of items) {
    const p = Promise.resolve().then(() => fn(item));
    results.push(p);
    if (executing.length >= limit) {
      await Promise.race(executing);
    }
    executing.push(p);
  }

  return Promise.all(results);
}

Top-Level await

In ES modules, await can be used at the top level without wrapping in an async function:

// module.mjs
const config = await fetch("/config.json").then(r => r.json());
export default config;

Common Pitfalls

PitfallProblemFix
forEach with async callbackDoes not await; runs in parallel silentlyUse for...of or Promise.all(items.map(...))
Missing awaitFunction returns a Promise instead of the valueAdd await
Awaiting inside a loop for independent callsSerializes what could be parallelCollect promises, then await Promise.all
Unhandled rejectionsCrashes in Node.jsWrap in try/catch or attach a global handler