Skip to main content

Command Palette

Search for a command to run...

Async/Await in JavaScript: Writing Cleaner Asynchronous Code

Updated
4 min read
Async/Await in JavaScript: Writing Cleaner Asynchronous Code

Why async/await Was Introduced

Historically, JavaScript handled asynchronous operations (like fetching data from a server or reading a file) using callbacks. When you had multiple operations depending on each other, callbacks nested inside callbacks created the infamous "callback hell" or "pyramid of doom," which was incredibly difficult to read and debug.

Promises were introduced to fix this by flattening the structure using .then() chaining. However, long chains of .then() and .catch() could still become visually cluttered and complicated, especially when passing variables down the chain or handling conditional logic.

async/await was introduced in ES2017 to solve this completely. It allows you to write asynchronous code that looks and behaves like traditional, synchronous code, reading top-to-bottom without the nesting.


How the async Keyword Works

The async keyword is placed before a function declaration. Doing this does one very specific thing: it guarantees that the function will return a Promise. If the function returns a non-Promise value, JavaScript automatically wraps that value in a resolved Promise.

// A standard async function
async function greet() {
  return "Hello, world!"; 
}

// Under the hood, this is essentially doing:
// return Promise.resolve("Hello, world!");

greet().then(message => console.log(message)); // Output: Hello, world!

The await Keyword Concept

The await keyword is where the magic happens. It can only be used inside an async function.

When JavaScript encounters await before a Promise, it literally pauses the execution of that specific function until the Promise settles (either resolves or rejects). While that function is paused, the rest of your JavaScript program (the main thread) continues running smoothly without blocking the browser or server.

Once the Promise resolves, await "unwraps" the Promise and returns the final value.

// A mock function that takes 2 seconds to resolve
function fetchGreeting() {
  return new Promise(resolve => setTimeout(() => resolve("Welcome back!"), 2000));
}

async function displayGreeting() {
  console.log("Fetching greeting...");
  
  // Execution pauses here for 2 seconds, but the rest of your app keeps running
  const greeting = await fetchGreeting(); 
  
  // This line only runs after the Promise resolves
  console.log(greeting); 
}

displayGreeting();

Comparison: Promises vs. async/await

The best way to see the readability improvement is to compare them side-by-side using a real-world scenario, like fetching user data from an API.

The Promise Way (Using .then chaining):

function getUserData() {
  fetch('https://jsonplaceholder.typicode.com/users/1')
    .then(response => {
      if (!response.ok) throw new Error('Network issue');
      return response.json();
    })
    .then(user => {
      console.log(`User name: ${user.name}`);
    })
    .catch(error => {
      console.error(`Failed to fetch user: ${error}`);
    });
}

The async/await Way:

async function getUserData() {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
    if (!response.ok) throw new Error('Network issue');
    
    const user = await response.json();
    console.log(`User name: ${user.name}`);
    
  } catch (error) {
    console.error(`Failed to fetch user: ${error}`);
  }
}

Why the second approach is cleaner:

  • No Callbacks: You don't have to pass anonymous functions into .then().

  • Linear Flow: The code reads from top to bottom, exactly like synchronous code.

  • Variable Scope: Variables like response and user are easily available in the same scope, whereas sharing variables between multiple .then() blocks can be tedious.


Error Handling with Async Code

With standard Promises, you handle errors by appending a .catch() block at the end of your chain.

With async/await, you get to use standard, synchronous try...catch blocks. This is a massive advantage because it unifies your error handling. You can handle standard synchronous errors (like a typo or undefined variable) and asynchronous errors (like a failed API call) in the exact same catch block.

async function processOrder(orderId) {
  try {
    // if any of these await statements fail, it immediately jumps to the catch block
    const orderDetails = await fetchOrder(orderId);
    const paymentStatus = await processPayment(orderDetails.total);
    const shippingInfo = await arrangeShipping(orderDetails.address);
    
    console.log("Order completely processed!", shippingInfo);
  } catch (error) {
    // handles network errors, declined payments, OR syntax errors in the try block
    console.error("Order processing failed:", error.message);
  }
}

JS Under the Hood: The Engine Room

Part 20 of 24

Ever wondered why your code actually runs? We’re going beyond the syntax to explore the V8 engine, memory management, and the "magic" that happens between your keyboard and the screen. Just deep dives.

Up next

Synchronous vs Asynchronous JavaScript

1. The Everyday Analogy: The Coffee Shop Imagine you walk into a local coffee shop. The way the shop is run perfectly illustrates these two concepts: The Synchronous Coffee Shop (Blocking) There is on

Async/Await in JavaScript: Writing Cleaner Asynchronous Code