JavaScript - async/await

Overview

Estimated time: 35–45 minutes

Async/await makes asynchronous code read like synchronous code. Learn how to handle errors, run tasks sequentially or concurrently, and manage timeouts and cancellation.

Learning Objectives

  • Refactor promise chains into clear async/await flows.
  • Use try/catch/finally to handle errors and cleanup.
  • Run tasks sequentially vs concurrently with Promise.all.
  • Implement timeouts and cancellation with Promise.race and AbortController.

Prerequisites

Basics

// An async function always returns a Promise
async function getValue() {
  return 42; // resolves with 42
}

async function run() {
  const v = await getValue();
  return v * 2; // resolves with 84
}

run().then(console.log);

Error handling

async function mayFail(flag) {
  if (flag) throw new Error('boom');
  return 'ok';
}

async function main() {
  try {
    const res = await mayFail(false);
    console.log(res);
  } catch (e) {
    console.error('caught:', e.message);
  } finally {
    console.log('cleanup');
  }
}

Sequential vs concurrent

const delay = (ms, v) => new Promise(res => setTimeout(() => res(v), ms));

// Sequential (waits each in turn ~ 300ms total)
async function seq() {
  const a = await delay(100, 'A');
  const b = await delay(100, 'B');
  const c = await delay(100, 'C');
  return [a, b, c];
}

// Concurrent (~ 100ms total)
async function conc() {
  const [a, b, c] = await Promise.all([
    delay(100, 'A'),
    delay(100, 'B'),
    delay(100, 'C'),
  ]);
  return [a, b, c];
}

// Mapping to promises, then await all
async function mapConcurrent(values) {
  const promises = values.map((v, i) => delay(50 + i*10, v*v));
  return await Promise.all(promises);
}

Timeouts and cancellation

// Timeout with Promise.race
async function withTimeout(promise, ms) {
  const timeout = new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), ms));
  return Promise.race([promise, timeout]);
}

// fetch + AbortController (browser environments)
async function fetchWithAbort(url, ms = 5000) {
  const ctrl = new AbortController();
  const timer = setTimeout(() => ctrl.abort(), ms);
  try {
    const res = await fetch(url, { signal: ctrl.signal });
    return await res.text();
  } finally {
    clearTimeout(timer);
  }
}

Top-level await

Top-level await works inside ES modules (<script type="module">) or modern build setups. Otherwise, wrap code in an async IIFE: (async () => { /* await ... */ })().

Common Pitfalls

  • Accidental serialization: await in a loop runs tasks sequentially. For parallelism, build an array of promises and await Promise.all.
  • Missing await: Forgetting await yields a pending promise, not the value.
  • Try/catch scope: Only errors thrown/awaited within the try are caught.

Try it

Run to compare sequential vs concurrent, and handle errors with try/catch: