Skip to main content

Command Palette

Search for a command to run...

Async/Await in JavaScript: Writing Cleaner Asynchronous Code

Async/Await: Making Asynchronous Code Look Synchronous ✨

Updated
4 min read
J

Turning chai into code and ideas into full-stack applications. Sharing lessons from my development journey, one commit at a time.

The Callback Hell Escape

Three months into my first job, I inherited this monstrosity:

fetchUser(userId, function(user) {
  fetchPosts(user.id, function(posts) {
    fetchComments(posts[0].id, function(comments) {
      fetchAuthor(comments[0].authorId, function(author) {
        console.log(author.name);
        // 4 levels deep and my eyes hurt 😵
      });
    });
  });
});

My senior looked at my screen and said: "Welcome to callback hell. Let me show you the future."

He refactored it to this:

async function getAuthorName(userId) {
  const user = await fetchUser(userId);
  const posts = await fetchPosts(user.id);
  const comments = await fetchComments(posts[0].id);
  const author = await fetchAuthor(comments[0].authorId);
  return author.name;
}

Same logic. One-tenth the pain. My jaw dropped.


What Problem Does Async/Await Solve?

JavaScript is single-threaded. When you fetch data from an API, you can't freeze the entire app waiting for the response. You need asynchronous code.

The Evolution:

  1. Callbacks (2009): Nested functions → Callback hell 🔥

  2. Promises (2015): .then() chains → Better, but still messy

  3. Async/Await (2017): Looks synchronous → Perfect! ✨

The Promise Pain:

fetchUser(userId)
  .then(user => fetchPosts(user.id))
  .then(posts => fetchComments(posts[0].id))
  .then(comments => fetchAuthor(comments[0].authorId))
  .then(author => console.log(author.name))
  .catch(error => console.error(error));

Better than callbacks, but:

  • Still chaining .then()

  • Hard to use if statements or loops

  • Error handling is separate (.catch())

The Async/Await Solution:

async function showAuthor(userId) {
  try {
    const user = await fetchUser(userId);
    const posts = await fetchPosts(user.id);
    const comments = await fetchComments(posts[0].id);
    const author = await fetchAuthor(comments[0].authorId);
    console.log(author.name);
  } catch (error) {
    console.error(error);
  }
}

Benefits:

  • Reads like synchronous code

  • Use normal if, for, try-catch

  • No chaining


How Async Functions Work

The async Keyword

Adding async before a function makes it return a Promise automatically:

// Regular function
function getData() {
  return "Hello";
}

console.log(getData()); // "Hello"

// Async function
async function getDataAsync() {
  return "Hello";
}

console.log(getDataAsync()); // Promise {<fulfilled>: "Hello"}

Key insight: async functions ALWAYS return Promises, even if you return a regular value!

The await Keyword

await pauses execution until the Promise resolves:

async function example() {
  console.log("Start");
  
  const result = await someAsyncOperation(); // Pauses here
  
  console.log("Result:", result); // Continues after Promise resolves
}

Rules:

  • await only works inside async functions

  • await pauses the function, NOT the entire program

  • Other code keeps running!


Real-World Examples

Example 1: Fetching API Data

// Old way with Promises
function getUserData(userId) {
  return fetch(`/api/users/${userId}`)
    .then(response => response.json())
    .then(data => {
      console.log(data);
      return data;
    })
    .catch(error => {
      console.error('Error:', error);
    });
}

// New way with Async/Await
async function getUserData(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);
    const data = await response.json();
    console.log(data);
    return data;
  } catch (error) {
    console.error('Error:', error);
  }
}

Example 2: Sequential vs Parallel

// SLOW: Sequential (waits for each)
async function getDataSequential() {
  const user = await fetchUser(1);       // Wait 1s
  const posts = await fetchPosts(1);     // Wait 1s
  const comments = await fetchComments(1); // Wait 1s
  // Total: 3 seconds
}

// FAST: Parallel (all at once)
async function getDataParallel() {
  const [user, posts, comments] = await Promise.all([
    fetchUser(1),
    fetchPosts(1),
    fetchComments(1)
  ]);
  // Total: 1 second (all run simultaneously)
}

Example 3: Error Handling

async function robustFetch(url) {
  try {
    const response = await fetch(url);
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    const data = await response.json();
    return data;
    
  } catch (error) {
    console.error('Fetch failed:', error.message);
    return null; // Graceful fallback
  }
}

Async/Await vs Promises

Feature Promises Async/Await
Readability Chaining .then() Looks synchronous
Error handling .catch() at end try-catch block
Conditions Awkward Natural if-else
Loops Hard Easy with for loop
Debugging Stack traces messy Clear stack traces

Interview Tips

Q: What is async/await?

A: "Async/await is syntactic sugar over Promises that makes asynchronous code look and behave more like synchronous code. The async keyword makes a function return a Promise, and await pauses execution until that Promise resolves. It was introduced in ES2017 to improve code readability and make error handling easier with standard try-catch blocks instead of .catch() chains."


Key Takeaways

  1. async functions always return Promises

  2. await pauses execution until Promise resolves

  3. Much more readable than Promise chains

  4. Use try-catch for errors

  5. await only works in async functions

  6. Use Promise.all() for parallel operations

  7. Async/await is just sugar over Promises