Understanding JavaScript async/await

What Is async/await?

JavaScript is a single-threaded language. If time-consuming tasks like network requests or file reads are handled synchronously, the UI freezes. Asynchronous programming solves this problem, and async/await is its core syntax.

async/await lets you write Promise-based asynchronous code as if it were synchronous, making it much easier to read. This article covers basic usage, error handling, parallel execution, and common real-world patterns.

From Promise to async/await

To understand async/await, you first need to know Promises. async/await is syntactic sugar over Promises and works identically under the hood.

// Promise chaining approach
function fetchUserPromise(id) {
  return fetch(`/api/users/${id}`)
    .then(response => response.json())
    .then(user => {
      console.log(user.name);  // "Alice"
      return user;
    });
}

// async/await approach (same behavior)
async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`);
  const user = await response.json();
  console.log(user.name);  // "Alice"
  return user;
}

Promise chaining becomes harder to follow as .then() calls nest deeper. async/await reads top-to-bottom sequentially, greatly improving readability.

AspectPromise Chainingasync/await
ReadabilityGets complex with nested .then()Sequential, similar to synchronous code
Error handling.catch() chainingtry/catch blocks
DebuggingUnclear stack tracesSame stack traces as regular functions
BranchingDifficult inside .then()Natural use of if/else

Basic Usage

Adding the async keyword before a function makes it always return a Promise. await can only be used inside async functions and pauses execution until the Promise resolves.

// async functions always return a Promise
async function getMessage() {
  return "Hello";  // Same as Promise.resolve("Hello")
}

getMessage().then(msg => console.log(msg));  // "Hello"

// await extracts the resolved value from a Promise
async function loadData() {
  console.log("Request started");
  const response = await fetch("/api/data");  // Wait for response
  const data = await response.json();         // Wait for JSON parsing
  console.log("Data received:", data.length, "items");
  return data;
}

await only pauses execution of the current async function — the browser’s other tasks (rendering, event handling) continue normally. This is the key difference from synchronous code.

Error Handling

Error handling is the most common source of mistakes in asynchronous code. async/await lets you handle errors using try/catch, the same pattern as synchronous code.

// Error handling with try/catch
async function fetchData(url) {
  try {
    const response = await fetch(url);

    // Check HTTP error status (fetch only rejects on network errors)
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }

    const data = await response.json();
    return data;
  } catch (error) {
    // Handles network errors + HTTP errors + JSON parsing errors
    console.error("Data request failed:", error.message);
    throw error;  // Propagate to caller
  }
}

// Handling errors at the call site
try {
  const data = await fetchData("https://api.example.com/users");
  console.log("User count:", data.length);  // User count: 42
} catch (error) {
  console.error("Final error:", error.message);
}

fetch() only rejects on network errors (server unreachable, DNS failure, etc.) — it does not reject on HTTP error statuses like 404 or 500. You must always check response.ok.

Parallel Execution: Promise.all and Promise.allSettled

Sequentially awaiting multiple independent async tasks makes them unnecessarily slow. Using Promise.all(), you can start all tasks simultaneously and wait for all of them to complete.

// Sequential execution: takes 2 seconds total (1s + 1s)
async function sequential() {
  const user = await fetchUser(1);      // Wait 1 second
  const posts = await fetchPosts(1);    // Wait 1 second
  return { user, posts };
}

// Parallel execution: takes 1 second total (started simultaneously)
async function parallel() {
  const [user, posts] = await Promise.all([
    fetchUser(1),     // Start simultaneously
    fetchPosts(1),    // Start simultaneously
  ]);
  return { user, posts };
}

Promise.all() rejects immediately if any one Promise fails. If you need to tolerate partial failures, use Promise.allSettled().

// Parallel execution that tolerates partial failures
const results = await Promise.allSettled([
  fetchUser(1),
  fetchUser(999),    // Non-existent user (may fail)
  fetchUser(2),
]);

// Handle success/failure individually
results.forEach((result, index) => {
  if (result.status === "fulfilled") {
    console.log(`#${index} succeeded:`, result.value.name);
  } else {
    console.log(`#${index} failed:`, result.reason.message);
  }
});
// #0 succeeded: Alice
// #1 failed: User not found
// #2 succeeded: Charlie
MethodBehavior on failureReturn valueWhen to use
Promise.allRejects immediately on any failureResult arrayWhen all must succeed
Promise.allSettledWaits until all complete{status, value/reason} arrayWhen partial failure is acceptable
Promise.raceResolves/rejects with the first settledFirst resultImplementing timeouts

Common Mistakes

Using await inside loops: Using await inside forEach results in unexpected parallel execution without waiting for completion. Use for...of for sequential execution, or Promise.all + map for parallel execution.

// Wrong: forEach does not wait for await
userIds.forEach(async (id) => {
  const user = await fetchUser(id);  // Completion is not guaranteed
});

// For sequential execution: use for...of
for (const id of userIds) {
  const user = await fetchUser(id);  // Processes one at a time in order
  console.log(user.name);
}

// For parallel execution: use Promise.all + map
const users = await Promise.all(
  userIds.map(id => fetchUser(id))  // All start simultaneously
);

This mistake is extremely common in practice. It occurs because forEach ignores the return value (Promise) of async callbacks.

Summary

async/await is the standard pattern for JavaScript asynchronous programming. Here are the key points:

  • async functions always return a Promise and allow await inside them
  • Error handling uses try/catch, the same as synchronous code
  • Independent tasks should use Promise.all() for parallel execution to improve performance
  • For partial failure tolerance, use Promise.allSettled()
  • In loops, use for...of or Promise.all + map instead of forEach
  • fetch() does not auto-reject on HTTP errors, so always check response.ok

Was this article helpful?