On this page

Async/Await

12 min read TextCh. 4 — Asynchrony

What is async/await?

async/await is syntactic sugar over promises that makes asynchronous code read as if it were synchronous. Introduced in ES2017, it is the preferred way to work with asynchronous operations in modern JavaScript.

The async keyword

Marking a function as async has two effects:

  1. The function always returns a promise
  2. It allows using await inside it
async function example() {
  return 42; // equivalent to return Promise.resolve(42)
}

The await keyword

await pauses the execution of the async function until the promise resolves, and returns its value:

async function getData() {
  const result = await somePromise(); // pauses here
  console.log(result); // runs when the promise resolves
}

Error handling

Instead of .catch(), we use try/catch blocks that are familiar from synchronous programming:

async function load() {
  try {
    const data = await riskyOperation();
    return data;
  } catch (error) {
    console.error('Failed:', error.message);
    return defaultValue;
  } finally {
    cleanupResources();
  }
}

The catch block catches both rejected promise errors and exceptions thrown with throw.

Sequential vs parallel

This is one of the most common mistakes with async/await:

Sequential (slow)

const a = await request1(); // waits 1s
const b = await request2(); // waits 1s more
// Total: 2 seconds

Parallel (fast)

const [a, b] = await Promise.all([request1(), request2()]);
// Total: 1 second (they run at the same time)

Use the sequential version only when one request depends on the result of the previous one.

Useful patterns

Processing a list of items

Depending on whether the items are independent or not:

  • Sequential — Use for...of with await inside the loop
  • Parallel — Use .map() with async functions and Promise.all

Retry with exponential backoff

A robust pattern for retrying operations that may temporarily fail (e.g., network requests). Each retry waits exponentially longer (1s, 2s, 4s...).

Top-level await

In ES modules, you can use await directly at the module scope:

// config.js (ES module)
const response = await fetch('/api/config');
export const config = await response.json();

async/await vs .then()

Aspect async/await .then()
Readability More readable, linear flow Can become nested
Errors Familiar try/catch .catch() in chain
Debugging Clear stack traces Fragmented stack traces
Parallelism Requires Promise.all Natural with multiple .then

In practice, async/await is preferred for most cases. Use .then() when you need quick promise composition or in short callbacks.


Practice

  1. Convert .then() to async/await: Take an existing promise chain with .then().catch() and rewrite it using async/await with try/catch/finally. Verify that the behavior is identical.
  2. Run tasks in parallel: Write an async function that uses Promise.all to execute 3 simulated calls (using setTimeout wrapped in promises) in parallel, and measure the total time with performance.now().
  3. Implement a basic retry: Create a function withRetries(fn, attempts) that retries executing fn up to attempts times if it fails. Test it with a function that fails randomly.

In the next lesson we will learn how to use the Fetch API to make HTTP requests.

Sequential vs parallel await
Multiple consecutive awaits execute sequentially. If the operations are independent, use Promise.all to run them in parallel and reduce the total time.
Top-level await
In ES modules (files with import/export), you can use await outside of async functions. This is useful for module initialization and scripts.
javascript
// Async function - always returns a promise
async function getUser(id) {
  // await pauses execution until the promise resolves
  const response = await fetch(`/api/users/${id}`);
  const user = await response.json();
  return user;
}

// Error handling with try/catch
async function loadData() {
  try {
    const user = await getUser(1);
    const posts = await fetch(`/api/users/${user.id}/posts`);
    const data = await posts.json();
    console.log('Posts:', data);
  } catch (error) {
    console.error('Error loading:', error.message);
  } finally {
    console.log('Loading finished');
  }
}

// Parallel requests with await
async function loadDashboard() {
  // BAD: sequential (slow)
  // const users = await fetch('/api/users').then(r => r.json());
  // const posts = await fetch('/api/posts').then(r => r.json());

  // GOOD: parallel (fast)
  const [users, posts] = await Promise.all([
    fetch('/api/users').then(r => r.json()),
    fetch('/api/posts').then(r => r.json()),
  ]);

  return { users, posts };
}

// Async arrow function
const getProfile = async (id) => {
  const res = await fetch(`/api/profiles/${id}`);
  if (!res.ok) {
    throw new Error(`HTTP ${res.status}`);
  }
  return res.json();
};
javascript
// Pattern: process items sequentially
async function processSequential(urls) {
  const results = [];
  for (const url of urls) {
    const res = await fetch(url);
    const data = await res.json();
    results.push(data);
  }
  return results;
}

// Pattern: process items in parallel
async function processParallel(urls) {
  const promises = urls.map(async (url) => {
    const res = await fetch(url);
    return res.json();
  });
  return Promise.all(promises);
}

// Pattern: retry with exponential backoff
async function withRetries(fn, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === attempts - 1) throw error;
      const wait = Math.pow(2, i) * 1000;
      console.log(`Retrying in ${wait}ms...`);
      await new Promise(r => setTimeout(r, wait));
    }
  }
}

// Using the retry
const data = await withRetries(
  () => fetch('/api/data').then(r => r.json()),
  3
);