DEV Community

Cover image for How Async/Await Made Me Love Asynchronous JavaScript Again
Chukwunonso Joseph Ofodile
Chukwunonso Joseph Ofodile

Posted on

How Async/Await Made Me Love Asynchronous JavaScript Again

If you’ve been in the JavaScript world for more than a minute, you’ve felt the pain of asynchronous code. It’s the bedrock of modern web development, powering everything from API calls to file reading, but for years, managing it was a chore. We went from the “Callback Hell” of nested functions to the slightly improved, but often still confusing, chains of .then() and .catch() with Promises.

If you enjoy this guide, I wrote a full ebook that goes deeper: Mastering JavaScript for AI Click here to get yours

Then, in 2017, a new syntax landed in ES8 that changed everything: async and await.

It wasn’t a new magic behind the scenes — it was syntactic sugar on top of Promises. But what glorious sugar it is! It allowed us to write asynchronous code that looks and behaves like synchronous code, making it infinitely more readable and maintainable.

Let me show you how it works and why it will become your new best friend.The “Before Times”: A Tale of Two Asynchronous Styles

The “Before Times”: A Tale of Two Asynchronous Styles
To appreciate async/await, let's first glance at what we dealt with before.

1. Callback Hell

The old way. A pyramid of doom that is hard to read and reason about.

getUser(id, function (user) {
  getPosts(user.id, function (posts) {
    getComments(posts[0].id, function (comments) {
      // And so it goes... deeper and deeper
      console.log(comments);
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

2. Promise Chains

A significant step up! Promises gave us a cleaner, chainable structure. But long chains could still become difficult to follow, and error handling could be tricky.

getUser(id)
  .then(user => getPosts(user.id))
  .then(posts => getComments(posts[0].id))
  .then(comments => console.log(comments))
  .catch(error => console.error('Something went wrong!', error));

Enter fullscreen mode Exit fullscreen mode

While this is better, we’re still “threading” values through callbacks. It’s functional, but it doesn’t feel natural.

The async/await Revolution
async and await are two keywords that work together to make Promise-based code feel synchronous.

The async Keyword
You place the async keyword before a function declaration. This does two things:

It forces the function to always return a Promise. If you return a non-Promise value, JavaScript automatically wraps it in a resolved Promise.
It enables the await keyword to be used inside that function.

async function myAsyncFunction() {
  return 42;
  // This is equivalent to: return Promise.resolve(42);
}

myAsyncFunction().then(alert); // 42
Enter fullscreen mode Exit fullscreen mode

The await Keyword
This is the real magic. The await keyword can only be used inside an async function. It pauses the execution of the async function and waits for a Promise to resolve. Once resolved, it resumes the function and returns the resolved value.

Key point: It pauses the function, not the whole JavaScript runtime. The rest of your application (event listeners, other async operations, etc.) remains perfectly responsive.

Let’s rewrite our Promise chain with async/await:

async function fetchUserData() {
  const user = await getUser(id);
  const posts = await getPosts(user.id);
  const comments = await getComments(posts[0].id);
  console.log(comments);
}

fetchUserData();
Enter fullscreen mode Exit fullscreen mode

If you enjoy this guide, I wrote a full ebook that goes deeper: Mastering JavaScript for AI Click here to get yours

Look at that! It’s clean, linear, and reads just like synchronous code. We assign the resolved values of our Promises to variables (user, posts) and use them in the next line. The code tells a clear story of what happens and in what order.

Error Handling: try...catch to the Rescue
This is one of the biggest wins. With Promise chains, you have a single .catch() at the end. With async/await, you can use the familiar try...catch block, which is much more powerful and flexible.

async function fetchUserData() {
  try {
    const user = await getUser(id);
    const posts = await getPosts(user.id);
    const comments = await getComments(posts[0].id);
    console.log(comments);
  } catch (error) {
    // This will catch ANY error thrown in the try block,
    // whether it's a network error, a runtime error, or a rejected Promise.
    console.error('Oops!', error);
  } finally {
    // This will run regardless of success or failure.
    console.log('Fetch operation attempted.');
  }
}

Enter fullscreen mode Exit fullscreen mode

This structure gives you fine-grained control. You can even wrap individual await statements in their own try...catch blocks if you need to handle specific errors differently.

Leveling Up: Parallelism and Performance
A common mistake when starting with async/await is unintentionally making your code slower. Look at this example:

async function slowSeries() {
  const user = await fetch('/api/users/1'); // takes 1 sec
  const posts = await fetch('/api/posts/1'); // takes 1 sec
  // Total time: ~2 seconds
}

Enter fullscreen mode Exit fullscreen mode

We wait for the user to finish fetching before we even start fetching the posts. This is sequential, and it’s inefficient if the two operations are independent.

The solution? Run them in parallel!

Since await is just waiting on a Promise, we can start the Promises first, then await their results.

async function fastParallel() {
  // Start both fetches immediately, they run concurrently.
  const userPromise = fetch('/api/users/1');
  const postsPromise = fetch('/api/posts/1');

  // Now we await their results. Both requests are already in flight.
  const user = await userPromise;
  const posts = await postsPromise;
  // Total time: ~1 second!
}
For an even cleaner approach, use Promise.all().

async function fastParallelWithPromiseAll() {
  // Kick off all promises simultaneously
  const [user, posts] = await Promise.all([
    fetch('/api/users/1'),
    fetch('/api/posts/1')
  ]);
  // Destructure the results array. Total time: ~1 second!
}

Enter fullscreen mode Exit fullscreen mode

Promise.all is your best friend for true, fire-and-forget parallelism. (Remember, it fails fast—if any promise rejects, the whole thing rejects).

Key Takeaways & Best Practices
async functions always return a Promise. This is non-negotiable. Remember to handle it with .then() or await when you call it.
await can only be used inside an async function. You'll get a syntax error otherwise. (Top-level await is now available in ES modules, but that's a topic for another day).
Don’t forget the await! A common beginner mistake is calling const data = fetch(...) without await. You'll just get a pending Promise assigned to data, not the actual response.
Use try...catch for error handling. It's the most robust and readable way to handle both Promise rejections and other errors.
Be mindful of sequential vs. parallel execution. Use Promise.all to run independent async operations concurrently for a major performance boost.
Conclusion
The async/await syntax didn't change how JavaScript's event loop works, but it fundamentally changed how we write asynchronous code. It took the powerful concept of Promises and made it accessible, readable, and less error-prone.

It turned this:

getUser(id)
  .then(user => getPosts(user.id))
  .then(posts => getComments(posts[0].id))
  .then(comments => console.log(comments))
  .catch(error => console.error('Error:', error));
Enter fullscreen mode Exit fullscreen mode

Into this:

async function displayComments() {
  try {
    const user = await getUser(id);
    const posts = await getPosts(user.id);
    const comments = await getComments(posts[0].id);
    console.log(comments);
  } catch (error) {
    console.error('Error:', error);
  }
}
Enter fullscreen mode Exit fullscreen mode

The second version is, in my opinion, a clear winner. It’s a joy to write and a gift to your future self (and your teammates) who will have to read it later.

So go ahead, refactor that old Promise chain. Embrace async/await. Your codebase will thank you for it.


If you enjoy this guide, I wrote a full ebook that goes deeper: Mastering JavaScript for AI. Click here to get yours

What are your thoughts on async/await? Have you run into any tricky situations while using it? Share your experiences in the comments below!

Top comments (0)