Skip to main content

Command Palette

Search for a command to run...

Async/Await in JavaScript: Writing Cleaner Asynchronous Code

Updated
8 min read
Async/Await in JavaScript: Writing Cleaner Asynchronous Code
H
CS undergrad | Tech enthusiast | Focusing on Web Dev • DSA • ML | Building skills for real-world impact

Async/Await in JavaScript: Writing Cleaner Asynchronous Code

Promises fixed callback hell. But chaining many .then() calls still looks messy. Async/await lets you write asynchronous code that reads like normal code.

If promises made async bearable, async/await made it obvious.


Why Async/Await Was Introduced

Promises solved callback hell, but promise chains can be hard to follow.

The Promise Chain Problem

function fetchUserData(userId) {
  return fetch(`/api/users/${userId}`)
    .then(response => response.json())
    .then(user => {
      return fetch(`/api/posts/${user.id}`)
        .then(response => response.json())
        .then(posts => ({ user, posts }));
    });
}

It works, but reading it requires jumping between .then() blocks.

The Async/Await Solution

async function fetchUserData(userId) {
  const response = await fetch(`/api/users/${userId}`);
  const user = await response.json();
  
  const postsResponse = await fetch(`/api/posts/${user.id}`);
  const posts = await postsResponse.json();
  
  return { user, posts };
}

Same logic, reads top to bottom like normal code.

Why It Matters

  • Easier to understand
  • Variables are in scope where you use them
  • Error handling with try/catch instead of .catch()
  • Feels like synchronous code

How Async Functions Work

An async function automatically returns a promise.

Basic Example

async function myFunction() {
  return "Hello";
}

myFunction().then(result => console.log(result));
// Output: Hello

Even though you return a string, the function returns a promise.

What Gets Returned

async function getNumber() {
  return 42;
}

const result = getNumber();
console.log(result);  // Promise { <pending> }

result.then(num => console.log(num));  // 42

Every async function returns a promise.

Two Ways to Call Async Functions

Option 1: With .then()

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

getMessage().then(msg => console.log(msg));

Option 2: With await (inside another async function)

async function main() {
  const msg = await getMessage();
  console.log(msg);
}

main();

Simple Async Example

async function processData() {
  const response = await fetch("/api/data");
  const data = await response.json();
  const processed = data.map(item => item * 2);
  return processed;
}

processData().then(result => console.log(result));

Each await pauses until the promise resolves.


The Await Keyword

The await keyword pauses execution until a promise resolves.

Basic Example

async function example() {
  console.log("Start");
  const result = await fetch("/api/data");
  console.log("End");
}

// Output:
// Start
// (waits for fetch)
// End

The function pauses at await and waits for the promise.

Multiple Awaits

async function loadUserPosts(userId) {
  const userResponse = await fetch(`/api/users/${userId}`);
  const user = await userResponse.json();
  
  const postsResponse = await fetch(`/api/posts/${user.id}`);
  const posts = await postsResponse.json();
  
  return { user, posts };
}

Each line waits for the previous one to finish.

Await Only Works in Async Functions

// Wrong: This doesn't work
function notAsync() {
  const data = await fetch("/api/data");  // SyntaxError
}

// Right: This works
async function isAsync() {
  const data = await fetch("/api/data");  // OK
}

You can only use await inside async functions.

Parallel Operations With Promise.all()

If operations don't depend on each other, run them together:

async function loadBoth() {
  const [user, posts] = await Promise.all([
    fetch("/api/users/1").then(r => r.json()),
    fetch("/api/posts/1").then(r => r.json())
  ]);
  
  return { user, posts };
}

This is faster than waiting for one, then the other.


Error Handling With Async Code

Use try/catch blocks with async/await.

Basic Try/Catch

async function fetchUser(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);
    const user = await response.json();
    return user;
  } catch (error) {
    console.log("Error:", error.message);
  }
}

fetchUser(1);

If any await fails, catch handles it.

Error Object Details

async function operation() {
  try {
    const result = await riskyFunction();
    console.log("Success:", result);
  } catch (error) {
    console.log("Message:", error.message);
    console.log("Type:", error.name);
    console.log("Stack:", error.stack);
  }
}

The error object has all the details.

Finally Block

async function processFile(filename) {
  let file;
  
  try {
    file = await openFile(filename);
    const content = await file.read();
    console.log("Content:", content);
  } catch (error) {
    console.log("Error:", error.message);
  } finally {
    if (file) {
      await file.close();  // Always close
    }
  }
}

The finally block always runs.

Handle Different Errors

async function complexOp() {
  try {
    const user = await getUser();
    const posts = await getPosts(user.id);
    return { user, posts };
  } catch (error) {
    if (error.message.includes("User not found")) {
      console.log("No user");
    } else if (error.message.includes("Network")) {
      console.log("Network problem");
    } else {
      console.log("Other error:", error);
    }
  }
}

Promise vs Async/Await

Both work the same way. Async/await is just cleaner syntax.

Side-by-Side Comparison

With Promises:

function getUser(userId) {
  return fetch(`/api/users/${userId}`)
    .then(response => response.json())
    .then(user => {
      console.log("User:", user);
      return user;
    })
    .catch(error => console.log("Error:", error));
}

With Async/Await:

async function getUser(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);
    const user = await response.json();
    console.log("User:", user);
    return user;
  } catch (error) {
    console.log("Error:", error);
  }
}

Async/await reads more naturally.

Readability Difference

Promises:

.then(...)
.then(...)
.then(...)
.catch(...)

Requires jumping between blocks

Async/Await:

await operation1
await operation2
await operation3
try/catch

Reads top to bottom

They're Compatible

You can use .then() with async/await:

async function loadData() {
  const response = await fetch("/api/data");
  return response.json();
}

// Still use .then()
loadData().then(data => console.log(data));

// Or await it
const data = await loadData();

Async/await doesn't replace promises. It's built on them.


Converting Promises to Async/Await

Example: Fetch Multiple Resources

Promise Version:

function loadUserWithPosts(userId) {
  let userData;
  
  return fetch(`/api/users/${userId}`)
    .then(response => response.json())
    .then(user => {
      userData = user;
      return fetch(`/api/posts/${user.id}`);
    })
    .then(response => response.json())
    .then(posts => {
      return { user: userData, posts };
    })
    .catch(error => {
      console.log("Error:", error);
      return null;
    });
}

Async/Await Version:

async function loadUserWithPosts(userId) {
  try {
    const userResponse = await fetch(`/api/users/${userId}`);
    const user = await userResponse.json();
    
    const postsResponse = await fetch(`/api/posts/${user.id}`);
    const posts = await postsResponse.json();
    
    return { user, posts };
  } catch (error) {
    console.log("Error:", error);
    return null;
  }
}

Much clearer. No temporary variables or nested callbacks.


Async Function Execution Flow

START
  |
  v
Enter async function
  |
  v
Execute line by line
  |
  ├─ await expression → pause
  |     |
  |     └─ wait for promise
  |           |
  |           └─ resolve
  |
  ├─ Continue
  |
  └─ await expression → pause
        |
        └─ wait for promise
              |
              └─ resolve
  |
  v
Return value (wrapped in promise)
  |
  v
END

Each await pauses execution. When the promise resolves, execution continues.


Real-World Example: API Call with Data Processing

async function fetchAndProcessUserData(userId) {
  try {
    // Fetch user
    console.log(`Fetching user ${userId}...`);
    const userResponse = await fetch(`/api/users/${userId}`);
    
    if (!userResponse.ok) {
      throw new Error(`HTTP error! status: ${userResponse.status}`);
    }
    
    const user = await userResponse.json();
    
    // Fetch posts
    console.log(`Fetching posts for ${user.name}...`);
    const postsResponse = await fetch(`/api/posts?userId=${user.id}`);
    const posts = await postsResponse.json();
    
    // Process data
    const result = {
      user: user.name,
      email: user.email,
      postCount: posts.length,
      recentPost: posts[0]?.title
    };
    
    console.log("Processing complete");
    return result;
    
  } catch (error) {
    console.error("Failed to fetch data:", error.message);
    return null;
    
  } finally {
    console.log("Operation finished");
  }
}

// Call it
fetchAndProcessUserData(1)
  .then(result => console.log("Result:", result))
  .catch(err => console.log("Outer error:", err));

Clean, readable, and easy to follow.


Practice Assignment

1. Convert a promise chain to async/await:

// Promise version
function getDataAndProcess() {
  return fetch("/api/data")
    .then(r => r.json())
    .then(data => processData(data))
    .catch(err => console.log(err));
}

// Convert to async/await

2. Handle errors with try/catch:

async function fetchMultiple() {
  // Fetch from /api/users and /api/posts
  // Handle any errors that occur
  // Log success and errors
}

3. Use parallel requests:

async function loadDashboard() {
  // Fetch users, posts, and comments at the same time
  // Use Promise.all() for parallel requests
  // Return all data combined
}

4. Chain dependent operations:

async function getUserPostComments(userId) {
  // Get user
  // Get user's posts
  // Get comments on first post
  // Return everything together
}

Quick Recap

  • Async/await is syntactic sugar for promises.

  • async functions always return a promise.

  • await pauses execution until a promise resolves.

  • await only works inside async functions.

  • Use try/catch for error handling.

  • finally block always runs.

  • Async/await is easier to read than promise chains.

  • Use Promise.all() to run operations in parallel.

  • Variables are in scope where you use them.

  • .then() and async/await can be mixed.

  • Async/await code reads like synchronous code.

Async/await makes promises easier to work with. Learn both and you'll truly understand JavaScript's async model.

Happy coding! 🚀


If you enjoyed this article, check out my other blogs on this profile. 🔗 Connect with me: LinkedIn | GitHub | X (Twitter)