Skip to content

Async Await

This lesson explains Async Await with clear examples, use cases, and best practices so you can use modern JavaScript confidently in real projects.

ES6 Async Await Overview

Async and await make asynchronous JavaScript easier to write and read. They are built on top of Promises and allow developers to write async code that looks similar to synchronous code.

Async await is commonly used with Fetch API, API calls, database requests, file uploads, timers, authentication flows, dashboards, form submissions, and any operation that takes time to complete.

Feature Description
async Declares a function that always returns a Promise.
await Pauses execution inside an async function until a Promise settles.
Error Handling Uses try, catch, and finally.
Built On Promises
Best Use Readable async code, API calls, sequential and parallel workflows.
Common APIs Fetch API, IndexedDB wrappers, Web APIs, server requests.

Basic Async Await Example

async function greetUser() {
  return "Hello User";
}

greetUser()
  .then((message) => {
    console.log(message);
  });

An async function always returns a Promise, even when it returns a normal value.

Using await

function waitOneSecond() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("Done waiting");
    }, 1000);
  });
}

async function runTask() {
  const result =
    await waitOneSecond();

  console.log(result);
}

runTask();

The await keyword waits for the Promise to finish before moving to the next line.

Async Await Syntax

Pattern Syntax Use Case
Async Function Declaration async function loadData() { ... } Reusable named async functions.
Async Function Expression const loadData = async function() { ... } Store async function in a variable.
Async Arrow Function const loadData = async () => { ... } Modern frontend callbacks and services.
Await Promise const data = await promise Read resolved Promise value.
Error Handling try { await task() } catch (error) { ... } Handle rejected Promises.

Async Await with Fetch API

async function loadUsers() {
  const response =
    await fetch("/api/users");

  const users =
    await response.json();

  console.log(users);
}

loadUsers();

This example waits for the API response, then waits again while converting the response body to JSON.

Fetch Error Handling with Async Await

async function loadProducts() {
  try {
    const response =
      await fetch("/api/products");

    if (!response.ok) {
      throw new Error(
        "HTTP Error: " + response.status
      );
    }

    const products =
      await response.json();

    return products;
  } catch (error) {
    console.error(
      "Failed to load products:",
      error.message
    );

    return [];
  }
}

Fetch does not reject for HTTP errors like 404 or 500. Check response.ok and throw an error when the request fails.

try catch finally with Async Await

async function submitForm(data) {
  try {
    console.log("Submitting form...");

    const response =
      await fetch("/api/contact", {
        method: "POST",
        headers: {
          "Content-Type": "application/json"
        },
        body: JSON.stringify(data)
      });

    if (!response.ok) {
      throw new Error("Form submit failed");
    }

    return await response.json();
  } catch (error) {
    console.error(error.message);

    return null;
  } finally {
    console.log("Submit process completed.");
  }
}

The finally block runs whether the async operation succeeds or fails. It is useful for hiding loaders, resetting buttons, or cleanup.

Async Arrow Functions

const getUser =
  async (id) => {
    const response =
      await fetch("/api/users/" + id);

    return response.json();
  }

getUser(101)
  .then((user) => {
    console.log(user);
  });

Async arrow functions are commonly used in services, event handlers, React hooks, and API utility files.

Sequential Await

async function loadDashboard() {
  const user =
    await fetch("/api/user")
      .then((response) => response.json());

  const orders =
    await fetch("/api/orders?userId=" + user.id)
      .then((response) => response.json());

  return {
    user,
    orders
  };
}

Sequential await is useful when the second request depends on the result of the first request.

Parallel Await with Promise.all()

async function loadPageData() {
  const userPromise =
    fetch("/api/user")
      .then((response) => response.json());

  const productsPromise =
    fetch("/api/products")
      .then((response) => response.json());

  const notificationsPromise =
    fetch("/api/notifications")
      .then((response) => response.json());

  const [user, products, notifications] =
    await Promise.all(
      [
        userPromise,
        productsPromise,
        notificationsPromise
      ]
    );

  return {
    user,
    products,
    notifications
  };
}

Use Promise.all() when async tasks are independent and can run at the same time.

Parallel Await with Promise.allSettled()

async function loadOptionalWidgets() {
  const results =
    await Promise.allSettled(
      [
        fetch("/api/weather"),
        fetch("/api/news"),
        fetch("/api/stocks")
      ]
    );

  results.forEach((result) => {
    if (result.status === "fulfilled") {
      console.log("Loaded:", result.value);
    } else {
      console.log("Failed:", result.reason);
    }
  });
}

Use Promise.allSettled() when some async tasks can fail without stopping the entire workflow.

Timeout Pattern with Promise.race()

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(
        new Error("Request timed out")
      );
    }, ms);
  });
}

async function loadWithTimeout() {
  try {
    const response =
      await Promise.race(
        [
          fetch("/api/data"),
          timeout(3000)
        ]
      );

    return response.json();
  } catch (error) {
    console.error(error.message);
    return null;
  }
}

Promise.race() can be used to stop waiting when an operation takes too long.

Async Await in Loops

async function loadUsersSequentially(ids) {
  const users =
    [];

  for (const id of ids) {
    const response =
      await fetch("/api/users/" + id);

    const user =
      await response.json();

    users.push(user);
  }

  return users;
}

Use for...of with await when each task must run one after another.

Async Await with map()

async function loadUsersInParallel(ids) {
  const userPromises =
    ids.map(async (id) => {
      const response =
        await fetch("/api/users/" + id);

      return response.json();
    });

  const users =
    await Promise.all(userPromises);

  return users;
}

map() with async returns an array of Promises. Use Promise.all() to wait for all results.

Async Event Handler Example

document
  .querySelector("#saveButton")
  .addEventListener("click", async () => {
    const button =
      document.querySelector("#saveButton");

    button.disabled =
      true;

    try {
      await submitForm(
        {
          name: "Alice",
          message: "Hello"
        }
      );

      console.log("Saved successfully.");
    } catch (error) {
      console.error("Save failed.");
    } finally {
      button.disabled =
        false;
    }
  });

Async event handlers are useful for forms, buttons, modals, file uploads, and UI workflows that call APIs.

Top-Level Await

// Works in JavaScript modules

const response =
  await fetch("/api/settings");

const settings =
  await response.json();

console.log(settings);

Top-level await can be used in ES modules. In normal scripts, use an async function wrapper.

Promises vs Async Await

Feature Promises Async Await
Syntax .then() and .catch() await with try catch
Readability Good for short chains. Better for complex flows.
Error Handling .catch() try catch
Parallel Work Promise.all() await Promise.all()
Return Value Promise Promise

Common Async Await Use Cases

  • Loading data from APIs.
  • Submitting forms.
  • Uploading files.
  • Authentication and login flows.
  • Loading dashboard widgets.
  • Processing multiple requests.
  • Retrying failed operations.
  • Waiting for browser APIs that return Promises.

Common Async Await Mistakes

  • Using await outside an async function or module.
  • Forgetting that async functions always return Promises.
  • Not using try catch for rejected Promises.
  • Running independent requests sequentially instead of in parallel.
  • Using async inside forEach() and expecting it to wait.
  • Forgetting to check response.ok with Fetch API.
  • Blocking UI without loading or error states.
  • Not using finally for cleanup.

Async Await Best Practices

  • Use try catch for error handling.
  • Use finally for cleanup and UI reset logic.
  • Use Promise.all() for independent parallel tasks.
  • Use for...of when async tasks must run sequentially.
  • Use meaningful function names for async operations.
  • Return useful fallback values after errors.
  • Show loading, success, and error states in the UI.
  • Keep async functions small and focused.

Key Takeaways

  • async functions always return Promises.
  • await pauses execution inside async functions until a Promise settles.
  • Use try catch to handle async errors.
  • Use Promise.all() for parallel async work.
  • Use for...of instead of forEach() when awaiting inside loops.
  • Async await makes Promise-based code easier to read and maintain.

Pro Tip

In interviews, explain async await as cleaner syntax over Promises: async returns a Promise, await waits for a Promise, try catch handles errors, and Promise.all() runs independent async tasks in parallel.