Why should you use async/await instead of the Promise.then() syntax? We'll examine two scenarios where async/await excels at writing asynchronous code.

By the end of this lesson, you'll understand how async/await improves the readability of your code.

Begin by cloning the starting code.

Code doesn't read from the top to bottom

Consider this example:

import { insertComment, insertHashtag } from "../utils/api.js";
import { extractHashtag } from "../utils/helpers.js";

const comment = "That is a very fine line of code. #dry";
const hashtag = extractHashtag(comment);

insertComment(comment)
  .then((commentId) => {
    console.log("start");
    return insertHashtag(hashtag, commentId);
  })
  .then((hashtagId) => {
    console.log("done");
  })
  .catch((error) => {
    console.log(error);
  });

console.log("end");

We have a comment from which we extract a hashtag. We then use the insertComment function to insert the comment, followed by using the comment ID to insert the hashtag. Finally, we log "done".

One of the first things we learn in programming is that code executes from top to bottom, similar to reading a book. So, when asked in what order the logs will print, you might say: start, done, end.

However, when we run this example, the logs appear in this order:

end
start
done

This is confusing and seems like the code execution "jumps" from line 19 to 9.

This is a drawback of the Promise.then() syntax—code reads less intuitively, and one has to jump back and forth in the code.

Accessing variables declared earlier in promise chains

Another drawback of Promise.then() syntax becomes apparent when trying to access variables declared inside previous .then() methods.

Suppose we want to log the comment ID when we're done:

insertComment(comment)
  .then((commentId) => {
    return insertHashtag(hashtag, commentId);
  })
  .then((hashtagId) => {
    console.log("Comment ID is", commentId);
  })

If we run this program, we get an error:

ReferenceError: commentId is not defined

This is because commentId is only available in the scope of the callback passed to its immediate .then(). Subsequent .then() methods don't have access to variables earlier in the promise chain.

In practice, accessing variables declared earlier in the promise chain is a common issue, especially in longer chains with multiple variables at different stages.

There are a few suboptimal solutions to this issue with the Promise.then() syntax. Let's examine both, and then compare with the async/await approach.

Approach #1 - Extract variable to outer scope

One solution is to extract the commentId variable to the outer scope so it can be accessed anywhere in the promise chain:

let commentId;
insertComment(comment)
  .then((newCommentId) => {
    commentId = newCommentId;
    return insertHashtag(hashtag, commentId);
  })
  .then((hashtagId) => {
    console.log("Comment ID is", commentId);
  })

For this, we have to rename the commentId variable inside the .then() method to prevent a name clash and assign commentId to newCommentId.

Imagine doing this not just for one, but for multiple variables spread out across different stages in the promise chain, and then coming up with new names for each one. It's tedious work and hard to track where the variables were created, assigned, and what values they hold.

This type of code is very hard to debug in production, as my experience has taught me.

Approach #2 - Passing variable to subsequent stage in the chain

Another approach is to have the first stage in the chain resolve with commentId. You can accomplish this by appending a then() method to the insertHashtag function and returning commentId:

insertComment(comment)
  .then((commentId) => {
    return insertHashtag(hashtag, commentId).then(() => {
      return commentId
    });
  })
  .then((commentId) => {
    console.log("Comment ID is", commentId);
  })

This solution, however, leads to nesting promises, which negates the advantage of chaining them and results in a pyramid of doom in our code.

Approach #3 - Refactor to async/await syntax

Here's the equivalent function using async/await syntax:

try {
  const commentId = await insertComment(comment);
  await insertHashtag(hashtag, commentId);
  console.log("Comment ID is", commentId);
} catch (error) {
  console.log(error);
}

Short and clear! Because everything lives in a single scope inside the try block, you can access as many variables as you'd like.

Check out the final solution.

Conditional asynchronous function

Begin by cloning the starting code.

Async/await also excels over Promise.then() syntax when you have a series of async functions, some of which are inside an if condition.

Here's another version of the previous example:

import { insertComment, insertHashtag } from "../utils/api.js";
import { extractHashtag } from "../utils/helpers.js";

const commentWithoutHashtag = "That is a very fine line of code.";
const hashtag = extractHashtag(commentWithoutHashtag);

insertComment(commentWithoutHashtag)
  .then((commentId) => {
    if (hashtag) {
      return insertHashtag(hashtag, commentId).then(() => {
        return commentId;
      });
    }

    return commentId;
  })
  .then((commentId) => {
    console.log("Comment ID is", commentId);
  })
  .catch((error) => {
    console.log(error);
  });

In this example, the comment doesn't have a hashtag, so hashtag will be null. We don't want to insert a non-existent hashtag, so we wrap insertHashtag inside an if statement.

However, we end up with a solution that nests promises, and there's no real way around this.

If we remove the then() method and the return statement:

insertComment(commentWithoutHashtag)
  .then((commentId) => {
    if (hashtag) {
      insertHashtag(hashtag, commentId); // if this rejects 
    }

    return commentId;
  })
  .then((commentId) => {
    console.log("Comment ID is", commentId);
  })
  .catch((error) => { // it won't be caught here
    console.log(error);
  });

When the insertHashtag function rejects, the error won't be caught by the catch() method.

And if we add the return statement:

insertComment(commentWithoutHashtag)
  .then((commentId) => {
    if (hashtag) {
      return insertHashtag(hashtag, commentId); 
    }

    return commentId;
  })
  .then((commentIdOrHashtagId) => {
    // ...
  })

The next value in the promise chain will be the comment ID, or the hashtag ID if there is a hashtag. This inconsistency is confusing and prone to bugs.

The only way is to append the then() method to insertHashtag and ensure the next value in the promise chain is always the comment ID.

Fast forwarding to the async/await solution:

try {
  const commentId = await insertComment(commentWithoutHashtag);
  if (hashtag) {
    await insertHashtag(hashtag, commentId);
  }
  console.log("Comment ID is", commentId);
} catch (error) {
  console.log(error);
}

This solution is simple and does what you expect it to do. If any of the functions reject with an error, it will be coherently handled inside the catch block. There is also no ambiguous variable.

Check out the final solution.

When you're done with this lesson, click on "Complete" then go to the next lesson to learn how to promisify callback-based asynchronous functions!