Async / Await in JavaScript

Async/await is syntax for working with Promises and asynchronous operations such as calling an API, reading a file, or waiting for a timer. An async function always returns a Promise. await pauses the surrounding async function until a Promise settles, without blocking the JavaScript thread or preventing other work from continuing.

Start here: async/await is built on Promises, so learn how a Promise resolves or rejects before continuing. Read the Promises tutorial first, then return here for the cleaner syntax.

Before async/await, developers mostly used Promises with .then() and .catch(). Async/await uses the same Promise system, but the code reads more like normal step-by-step logic.

javascript

// Without async/await (using .then)
fetch("https://jsonplaceholder.typicode.com/todos/1")
  .then(response => response.json())
  .then(data => console.log(data.title))
  .catch(err => console.error(err));

// With async/await (same thing, easier to read)
async function getPost() {
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
    const data = await response.json();
    console.log(data.title);
  } catch (err) {
    console.error(err);
  }
}
getPost();

Both examples do the same thing. But the async/await version reads top to bottom, just like normal code. No nested callbacks. No chain of .then(). Much easier to follow.

When you put async in front of a function, two things happen:

  • The function always returns a Promise, even if you return a plain value
  • You can use the await keyword inside that function

javascript

async function greet() {
  return "Hello!";
}

// Even though we returned a plain string,
// the function returns a Promise
greet().then(message => console.log(message)); // Hello!

You can use async with any type of function:

javascript

// Regular function
async function greet() {
  return "Hello!";
}

// Arrow function
const greetArrow = async () => {
  return "Hello!";
};

// Object method
const user = {
  getRole: async function() {
    return "admin";
  }
};

// Class method
class UserService {
  async getUser() {
    return { role: "admin" };
  }
}
Remember: You do not always need to call .then() on an async function. If you are calling it from inside another async function, just use await in front of it.

await pauses the execution of the surrounding async function until the Promise settles. If it fulfills, await evaluates to the fulfilled value. If it rejects, await throws the rejection reason. This pause does not block the JavaScript thread or the rest of the application.

javascript

function wait(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function run() {
  console.log("Start");
  await wait(2000); // pauses here for 2 seconds
  console.log("Done after 2 seconds");
}

run();
console.log("This runs immediately - not blocked");

You can use await inside an async function. Modern JavaScript also allows await at the top level of an ES module; in a classic script or regular function, using it outside those contexts causes a syntax error.

javascript

async function loadData() {
  const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
  const data = await response.json();
  console.log(data.title); // delectus aut autem
  console.log(data.completed); // false
}

loadData();
Watch out: When you await operations one by one inside a loop, each one waits for the previous to finish. If the operations are independent, use Promise.all() to start them concurrently and wait for all of them to complete.

A common mistake is forgetting await. In that case, the variable contains a Promise instead of the resolved value. A Promise is truthy, so a check such as if (data) can pass even when data is not the data you expected.

When a Promise rejects (something goes wrong), await throws an error. To handle it, wrap your await calls inside a try/catch block.

try/catch acts as a safety net. You try to do something that might fail - if it does, the catch block runs, and your app does not crash.

javascript

async function getUser(id) {
  try {
    const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
    if (!response.ok) {
      throw new Error("User not found");
    }
    const user = await response.json();
    console.log(user.name);
  } catch (error) {
    console.error("Something went wrong:", error.message);
  }
}

getUser(1);   // Leanne Graham
getUser(999); // Something went wrong: User not found

You can also add a finally block for code that should always run, no matter what happens - like hiding a loading spinner:

javascript

async function loadData() {
  try {
    const res = await fetch("https://jsonplaceholder.typicode.com/posts/1");
    const data = await res.json();
    console.log(data.title);
  } catch (err) {
    console.error("Failed to load:", err.message);
  } finally {
    console.log("Loading finished"); // always runs
  }
}

loadData();
Note: fetch() only rejects when there is a network failure. A 404 or 500 response does not throw an error by default. Always check response.ok or response.status manually.

Use the right error pattern: check response.ok for an HTTP error, use catch for network or rejected-Promise errors, and use finally to reset loading state. Do not show a successful empty page when a request failed.

javascript

async function loadProfile() {
  const status = document.querySelector("#status");
  status.textContent = "Loading...";

  try {
    const response = await fetch("/api/profile");
    if (!response.ok) {
      throw new Error(`Server returned ${response.status}`);
    }
    const profile = await response.json();
    status.textContent = profile.name;
  } catch (error) {
    status.textContent = "Could not load your profile. Try again.";
    console.error(error);
  } finally {
    document.querySelector("#retry-button").disabled = false;
  }
}

This pattern separates user feedback from developer diagnostics: the page gets a helpful message, while the console keeps the technical error for debugging.

Async/await does not replace Promises - it is built on top of them. Under the hood, an async function returns a Promise. await pauses the surrounding async function until that Promise settles. You are writing the same asynchronous logic with different syntax.

javascript

// Using Promises (.then chains)
function getPost() {
  return fetch("https://jsonplaceholder.typicode.com/posts/1")
    .then(res => res.json())
    .then(data => {
      console.log(data.title);
    })
    .catch(err => console.error(err));
}

// Same thing using async/await
async function getPost() {
  try {
    const res = await fetch("https://jsonplaceholder.typicode.com/posts/1");
    const data = await res.json();
    console.log(data.title);
  } catch (err) {
    console.error(err);
  }
}

Both versions produce the same result. Use async/await when your code has multiple steps that depend on each other. Use Promise.all() when you want to start multiple independent asynchronous operations concurrently and wait for all of them to complete:

javascript

// Running multiple independent tasks concurrently
async function loadDashboard() {
  const [users, posts] = await Promise.all([
    fetch("https://jsonplaceholder.typicode.com/users").then(r => r.json()),
    fetch("https://jsonplaceholder.typicode.com/posts").then(r => r.json())
  ]);
  console.log("Users:", users.length);  // 10
  console.log("Posts:", posts.length);  // 100
}

loadDashboard();
Tip: When you have independent tasks, use Promise.all() instead of awaiting each one before starting the next. It starts the operations concurrently and waits for all of them to finish.

Important: start all independent requests before awaiting them. Awaiting the first request before creating the second one makes them run one after another.

javascript

async function loadDashboard() {
  const requests = [
    fetch("https://jsonplaceholder.typicode.com/users").then(checkResponse),
    fetch("https://jsonplaceholder.typicode.com/posts").then(checkResponse)
  ];

  try {
    const [usersResponse, postsResponse] = await Promise.all(requests);
    const [users, posts] = await Promise.all([
      usersResponse.json(),
      postsResponse.json()
    ]);
    return { users, posts };
  } catch (error) {
    console.error("Dashboard could not load:", error.message);
    return null;
  }
}

function checkResponse(response) {
  if (!response.ok) {
    throw new Error(`Request failed with ${response.status}`);
  }
  return response;
}

Promise.all() rejects as soon as one request rejects. That is useful when the page needs every piece of data, but it is not ideal when one failed panel should not hide the others.

javascript

const results = await Promise.allSettled([
  fetch("/api/weather").then(checkResponse),
  fetch("/api/news").then(checkResponse)
]);

for (const result of results) {
  if (result.status === "fulfilled") {
    console.log("Panel loaded", result.value);
  } else {
    console.error("One panel failed", result.reason);
  }
}

Choose deliberately: use Promise.all() when all results are required; use Promise.allSettled() when each result can succeed or fail independently and you want to render partial results.

Guideline: Use async/await when it makes asynchronous control flow easier to read. .then() and .catch() are still valid and useful, especially for short Promise chains or when an async function is not needed.

  • async makes a function always return a Promise
  • await pauses the surrounding async function until a Promise settles and gives you its fulfillment value when successful
  • await does not block the JavaScript thread; other JavaScript work can continue
  • Use try/catch to handle rejected Promises, including network failures and errors you throw for HTTP responses
  • Add a finally block for cleanup code that runs whether the operation succeeds or fails
  • Async/await is built on top of Promises - they are not competing concepts
  • Use Promise.all() for independent concurrent operations when all results are required
  • Use Promise.allSettled() when independent operations can succeed or fail and every result should be inspected
  • await can be used inside an async function or at the top level of an ES module
Practice the prerequisite

Review Promises first, then practise the same async error-handling ideas with these exercises.

Promises tutorial Promise exercise 1 Promise exercise 2 Promise exercise 3

What's next? Now that you know how async and await work, let's use them with real HTTP requests in the next tutorial.

Videos for this topic will be added soon.

Reviewed by

SimplyJavaScript Editorial Team

Technical editors and JavaScript educators with hands-on experience building frontend projects, writing learning material, and reviewing tutorials for clarity, accuracy, and beginner-friendly guidance.