Async/Await

Learn how async/await makes working with asynchronous code in JavaScript easier and more readable.

Loading...
Async/Await

Before learning about async/await, it's important to know what asynchronous programming means.

What is Asynchronous Programming?

In JavaScript, code normally runs line by line, waiting for each operation to complete before moving to the next one, that is synchronous programming.

However, some operations like fetching data from a server, reading files, or waiting for user input, take time. If the code will run synchronously (line by line), then all these time taking operations will block the rest of your code. But asynchronous programming allows JavaScript to handle these time taking operations without blocking the rest of your code.

Asynchronous programming means letting JavaScript work on multiple tasks at the same time without waiting for each one to finish before starting the next.


What is async/await?

async and await are special JavaScript keywords that make it easier to work with code that takes time.

They are built on top of Promises and help you write asynchronous code in a way that looks and behaves like regular line by line code (synchronous code).

This makes your code simpler to read, write, and fix.


Why use async/await?

Before async/await, JavaScript used callbacks and Promises to handle asynchronous operations. But callbacks could lead to callback hell, and Promises, though better, sometimes got complex with chaining and represents a future value, something that will be available later.

Learn more about Promises and Callback hell!

async/await solves these problems by letting you write asynchronous code that:

  • Looks synchronous: line by line.
  • Is easier to debug: you can use normal try/catch blocks.
  • Avoids deep nesting: no more pyramid of doom or callback hell.

Let's see this difference by an example.

Example of a Promise:

const fetchUserData = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ name: "Shefali", age: 26 });
    }, 2000);
  });
};
 
// Using the Promise with .then()
fetchUserData()
  .then((user) => {
    console.log(user);
  })
  .catch((error) => {
    console.error(error);
  });

This works, but can become messy with multiple asynchronous operations (callback hell).

Example of async/await:

const fetchUserData = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ name: "Shefali", age: 26 });
    }, 2000);
  });
};
 
// Using the async/await
async function getUser() {
  try {
    const user = await fetchUserData();
    console.log(user);
  } catch (error) {
    console.error(error);
  }
}
 
getUser();

As you can see, both Promises and async/await do the same thing, they handle asynchronous code.

The difference is how they look:

  • Promises with .then() can lead to nested chains if you’re doing multiple steps.
  • async/await gives a cleaner, more readable structure, it looks like regular line by line code.

How to use async/await

The async Keyword

The async keyword is placed before a function declaration. It tells JavaScript that this function will handle asynchronous operations.

An async function always returns a Promise.

Note: To use await inside a function, it's necessary to first mark that function as async.

async function fetchData() {
  // You can use await here
}

The await Keyword

Inside an async function, you put await before a Promise to wait for it to finish. The code waits at that line and then continues when the result is ready.

async function fetchData() {
  const result = await fetch("https://api.example.com/data");
  const data = await result.json();
  console.log(data);
}
 
fetchData();

Here,

  • await fetch(...) waits for the fetch Promise to resolve.
  • await result.json() waits for the JSON parsing Promise.
  • Execution pauses at each await until the Promise resolves.
  • This makes async code look like normal synchronous code.

Error Handling

Handling errors with try/catch

Error handling with async/await is easier and more readable. You can use a regular try...catch block, just like in synchronous code, to catch errors from awaited Promises.

async function getUser() {
  try {
    const response = await fetch("/user");
    if (!response.ok) {
      throw new Error("Network response was not ok!");
    }
    const user = await response.json();
    console.log(user);
  } catch (error) {
    console.error("There was a problem:", error);
  }
}
 
getUser();

Here,

  • The function getUser is declared using async. This means you can use await inside it to pause until a Promise resolves.
  • In the try block, await fetch("/user") sends a request to /user and waits for the response. The function pauses here until the network call completes.
  • If response.ok is false (e.g., 404 or 500), it throws an error manually.
  • If the response is okay, then await response.json() parses the JSON body and waits for it to finish.
  • console.log(user) prints the user data to the console.
  • catch block handles errors. Any error from the fetch or JSON parsing (or the manual throw) is caught and logged.
  • This makes your async code clean, easy to read, and easier to debug.

Handling Multiple Errors

async function getUser() {
  try {
    const response = await fetch("/user");
 
    // Handle HTTP errors (non-2xx responses)
    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }
 
    // Try parsing JSON
    const user = await response.json();
 
    console.log(user);
  } catch (error) {
    // Handle all possible errors here
    if (error instanceof TypeError) {
      // Likely a network error or invalid JSON
      console.error("Network or parsing error:", error.message);
    } else {
      // Custom or HTTP error
      console.error("Something went wrong:", error.message);
    }
  }
}
 
getUser();

Here,

  • The function getUser is declared using async. This means you can use await inside it to pause until a Promise resolves.
  • In the try block, await fetch("/user") sends a request to /user and waits for the response. The function pauses here until the network call completes.
  • if (!response.ok)
    • Checks if the HTTP status is not in the 200–299 range.
    • Throws a custom error with the HTTP status if it fails.
  • If the response is okay, then await response.json() parses the JSON body and waits for it to finish.
  • console.log(user) prints the user data to the console.
  • catch (error) catches any error from fetch.
  • Error type checking:
    • TypeError: Usually means network issues or invalid JSON.
    • Anything else (like your custom HTTP error) is handled separately.
  • This pattern gives you fine control and clear debugging info for different types of failures, all while keeping the flow clean.

Using async/await with Multiple Promises

Sometimes, you need to make multiple asynchronous calls, like fetching user details and their posts. You could await each one separately.

For example:

async function fetchAll() {
  try {
    const user = await fetchUserData(1);
    const posts = await fetchPostsByUser(1);
 
    console.log("User:", user);
    console.log("Posts:", posts);
  } catch (error) {
    console.error("Something failed:", error);
  }
}

But this runs fetchUserData and fetchPostsByUser one after the other, which can be slow.

So the better way to do is by using Promise.all with await.

For example:

async function fetchAll() {
  try {
    const [user, posts] = await Promise.all([
      fetchUserData(1),
      fetchPostsByUser(1),
    ]);
 
    console.log("User:", user);
    console.log("Posts:", posts);
  } catch (error) {
    console.error("Something failed:", error);
  }
}

Using Promise.all() with async/await lets you run multiple async tasks at the same time, and still keeps your code clean and easy to read.

How It Works

  • fetchUserData(1) and fetchPostsByUser(1) both return Promises.
  • Promise.all([...]) runs them at the same time.
  • await pauses until all Promises resolve.
  • The [user, posts] destructures the result into two variables.
  • If any Promise fails, the catch block runs.

Why This Approach is Better?

This approach is better because:

  • Both requests start at the same time.
  • You get both results together as an array.
  • This is easy to handle both results and errors in a single try/catch.

Tip

If one of the Promises fails, then all the others promises are ignored, and the error is caught.

To handle all results including failures, use Promise.allSettled() instead:

const results = await Promise.allSettled([
  fetchUserData(1),
  fetchPostsByUser(1),
]);

Examples of async/await

Example 1: Simple Async Function

// Simulating an API call
function fetchData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("Data received!");
    }, 2000);
  });
}
 
// Using async/await
async function getData() {
  console.log("Fetching data...");
  const result = await fetchData();
  console.log(result);
  console.log("Done!");
}
 
getData();
 
// Output:
// Fetching data...
// After 2 seconds:
// Data received!
// Done!

Here,

  • fetchData() simulates an API call and returns a Promise that resolves with "Data received!" after 2 seconds.
  • getData() is an async function.
  • It logs "Fetching data...".
  • Then it waits (await) for fetchData() to finish.
  • After the Promise resolves, it logs the result and "Done!".

Example 2: Multiple Async Operations

// Simulate API call to fetch user info
function fetchUser() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id: 1, name: "John", email: "john@example.com" });
    }, 1000);
  });
}
 
// Simulate API call to fetch user's posts
function fetchUserPosts(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { id: 101, title: "Async in JS", userId },
        { id: 102, title: "Promises Made Simple", userId },
      ]);
    }, 1000);
  });
}
 
// Simulate API call to fetch user's friends
function fetchUserFriends(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { id: 201, name: "Mark" },
        { id: 202, name: "Joe" },
      ]);
    }, 1000);
  });
}
 
async function fetchUserProfile() {
  try {
    console.log("Fetching user info...");
    const user = await fetchUser();
 
    console.log("Fetching user posts...");
    const posts = await fetchUserPosts(user.id);
 
    console.log("Fetching user friends...");
    const friends = await fetchUserFriends(user.id);
 
    return {
      user,
      posts,
      friends,
    };
  } catch (error) {
    console.error("Error:", error);
  }
}

Here,

  • fetchUser() returns a user after 1 second.
  • fetchUserPosts(userId) returns a list of posts for that user.
  • fetchUserFriends(userId) returns a list of that user's friends.
  • fetchUserProfile() is marked async, so you can use await inside it.
  • In the try block,
    • Fetch user info:
      • Logs "Fetching user info...".
      • Waits for fetchUser() to resolve.
      • Stores the result in user.
    • Fetch user posts:
      • Logs "Fetching user posts...".
      • Uses the user.id to fetch posts.
      • Waits for fetchUserPosts(user.id) to complete.
      • Stores the result in posts.
    • Fetch user friends:
      • Logs "Fetching user friends...".
      • Again uses user.id to fetch friends.
      • Waits for fetchUserFriends(user.id) to complete.
      • Stores the result in friends.
    • Combines user, posts, and friends into one object and returns it.
  • In the catch block,
    • If any awaited call fails, the catch block runs.
    • It logs the error with console.error.

Now you might be thinking, can you use Promise.all for some of these?

Here, posts and friends don't depend on each other, you can run them in parallel using Promise.all for better performance.

const [posts, friends] = await Promise.all([
  fetchUserPosts(user.id),
  fetchUserFriends(user.id),
]);

This makes your code both fast and clean.

Example 3: Fetching Data from an API

async function getWeatherData(city) {
  try {
    const response = await fetch(
      `https://api.weather.com/v1/weather?q=${city}`
    );
 
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
 
    const weatherData = await response.json();
    return weatherData;
  } catch (error) {
    console.error("Failed to fetch weather data:", error);
    throw error;
  }
}
 
// Using the function
async function displayWeather() {
  try {
    const weather = await getWeatherData("London");
    console.log(`Temperature: ${weather.temperature}°C`);
    console.log(`Condition: ${weather.condition}`);
  } catch (error) {
    console.log("Could not display weather information");
  }
}

Here,

  • The function getWeatherData(city) takes a city name and makes a fetch request. The await pauses the function until the response is received.
  • if (!response.ok) {...} checks if the response is not OK (e.g. 404 or 500). If so, it throws an error to be caught in the catch block.
  • If the response is okay, response.json() parses the JSON and returns the data.
  • In the catch block, any errors (like network failures or bad responses) are caught and rethrown so they can be handled elsewhere.
  • The displayWeather() function calls getWeatherData with "London" and waits until the data is received.
  • console.log(Temperature:...); and console.log(Condition:...); prints the temperature and condition from the response.
  • In the catch block, if anything goes wrong (e.g. city not found or fetch fails), it prints a fallback message.

Example 4: Reading Files (Node.js)

const fs = require("fs").promises;
 
async function readConfigFile() {
  try {
    const data = await fs.readFile("config.json", "utf8");
    const config = JSON.parse(data);
    return config;
  } catch (error) {
    if (error.code === "ENOENT") {
      console.error("Config file not found");
    } else {
      console.error("Error reading config file:", error);
    }
  }
}

Here,

  • const fs = require("fs").promises; imports the fs module's promise-based API.
  • async function readConfigFile() { ... } declares an asynchronous function to read and parse a JSON config file.
  • In the try block:
    • const data = await fs.readFile(...);
      • Reads config.json as a UTF-8 string.
      • await pauses until the file is read.
    • const config = JSON.parse(data); parses the JSON data into a JavaScript object and stores it inside config.
    • return config; returns the config.
  • In the catch block:
    • if (error.code === "ENOENT") {...}
      • Checks if the file doesn’t exist (ENOENT).
      • Prints a clear message "Config file not found".
    • else {...} handles other possible errors (e.g. JSON parsing issues, permission problems).

Common Mistakes with async/await

1. Using async/await inside Array methods like map

// This looks okay, but doesn't work as expected:
const results = items.map(async (item) => {
  return await processItem(item);
});
console.log(results); // This logs an array of Promises, not the processed (actual) results
 
// To get the processed results, use Promise.all with await:
const results = await Promise.all(
  items.map(async (item) => {
    return await processItem(item);
  })
);

map doesn’t wait for async callbacks to finish. It returns an array of Promises. Promise.all waits for all Promises to resolve, so you get actual results.

2. Forgetting to handle errors with try/catch

// No error handling here:
async function riskyFunction() {
  await mightFailOperation(); // If it fails, your app crashes silently
}
 
// Always use try/catch to catch errors:
async function saferFunction() {
  try {
    await mightFailOperation();
  } catch (error) {
    console.error("Operation failed:", error);
  }
}

Without try/catch, errors cause unhandled promise rejections, making bugs harder to track.

3. Using await in Non-Async Functions

// This will cause a syntax error
function regularFunction() {
  const result = await fetchData(); // SyntaxError: await only works inside async functions
}
 
// Mark the function async to use await inside:
async function asyncFunction() {
  const result = await fetchData();
}

await only works inside functions declared with async. Otherwise, JavaScript throws an error.

4. Mixing async/await with .then() chains

// Bad: Mixing async/await with .then() creates confusion
async function mixedApproach() {
  const user = await fetchUser();
 
  // Don't do this - mixing .then() with async/await
  return fetchUserPosts(user.id)
    .then((posts) => {
      return { user, posts };
    })
    .catch((error) => {
      console.error("Error fetching posts:", error);
    });
}
 
// Good: Stick to async/await consistently
async function consistentApproach() {
  try {
    const user = await fetchUser();
    const posts = await fetchUserPosts(user.id);
    return { user, posts };
  } catch (error) {
    console.error("Error fetching posts:", error);
    throw error;
  }
}

5. Not understanding that async functions always return Promises

// This function ALWAYS returns a Promise, even if you don't explicitly return one
async function getUserName() {
  return "John Doe"; // This actually returns Promise.resolve("John Doe")
}
 
// Bad: Trying to use the result directly
function badExample() {
  const name = getUserName(); // This is a Promise, not "John Doe"
  console.log(name); // Logs: Promise { <resolved>: "John Doe" }
  console.log(`Hello, ${name}`); // Logs: "Hello, [object Promise]"
}
 
// Good: Always await async function results
async function goodExample() {
  const name = await getUserName(); // Now this is "John Doe"
  console.log(name); // Logs: "John Doe"
  console.log(`Hello, ${name}`); // Logs: "Hello, John Doe"
}
 
// Or use .then() if you're not in an async function
function alternativeGoodExample() {
  getUserName().then((name) => {
    console.log(`Hello, ${name}`); // Logs: "Hello, John Doe"
  });
}

Best Practices for async/await

1. Always use try/catch for error handling

// Good: Catch errors so your app won’t crash silently
async function goodExample() {
  try {
    const result = await someAsyncOperation();
    return result;
  } catch (error) {
    console.error("Error:", error);
    throw error; // Optional: re-throw to let the caller handle it
  }
}
 
// Bad: No error handling, so failures go unnoticed
async function badExample() {
  const result = await someAsyncOperation(); // What if this fails?
  return result;
}

2. Don't forget to use await

// Good: Wait for the Promise to resolve before using the result
async function goodExample() {
  const result = await fetchData();
  console.log(result);
}
 
// Bad: Missing await means you get a Promise object, not the data
async function badExample() {
  const result = fetchData(); // This returns a Promise, not the actual data
  console.log(result); // Logs: Promise { <pending> }
}

3. Use Promise.all for independent operations

// Good: Run multiple tasks in parallel to save time
async function goodExample() {
  const [users, posts, comments] = await Promise.all([
    fetchUsers(),
    fetchPosts(),
    fetchComments(),
  ]);
  return { users, posts, comments };
}
 
// Bad: Runs tasks one after another, wasting time
async function badExample() {
  const users = await fetchUsers();
  const posts = await fetchPosts();
  const comments = await fetchComments();
  return { users, posts, comments };
}

4. Handle errors appropriately

async function processUserData(userId) {
  try {
    const user = await fetchUser(userId);
    const processedData = await processData(user);
    return processedData;
  } catch (error) {
    console.error("Processing failed:", error);
 
    // Return a default value or re-throw based on your needs
    return { error: "Failed to process user data" };
  }
}

👉 Next tutorial: Fetch API

Support my work!