Unlocking the Potential of Async in JavaScript

Lucas Porfirio - Jun 9 '23 - - Dev Community

Today I was wandering around Dev.to and found this great article about promises, and it gave me the idea to discuss about asynchronous Javascript.

As you should know by now, JS is standardized by ECMA International, and every year they develop new features to the language we all know and love. And in 2017, with the 8th version of ECMAScript(ES8), they announced a new syntactic suggar for our Promises: Async and Await.

Async/await is a newer way to handle Promises within our code, avoiding the creation of cascades of .then as we often see on our codes. It's worth noting that we are still working with Promises, but they become less visible and less verbose.

Syntax

We use the prefix async before defining a function to indicate that we are dealing with asynchronous code, and with the added prefix, we can use await before Promises to indicate a point to be awaited by the code. Let's understand how this works in practice:

// Promise way (pre ES8)
function fetchBooks(book) {
  fetch(`/books/${book}`).then(response => {
    console.log(response);
  });
}

// Async/Await way (ES8+)
async function fetchBooks(book) {
  const response = await fetch(`/books/${book}`);
  console.log(response);
}
Enter fullscreen mode Exit fullscreen mode

See how the code became clearer. We no longer need to declare .then or .catch and worry about a Promise not executing before using its result because await takes on the role of waiting for the request to return its result.

I believe that the biggest problem solved by async/await is the famous Promise cascade, affectionately called Promise Hell. Let's imagine the following code written in ES6 (without async/await):

fetch('/users/lukeskw').then(user => {
  fetch(`/groups/${user.id}`).then(groups => {
    groups.map(group => {
      fetch(`/group/${group.id}`).then(groupInfo => {
        console.log(groupInfo);
      });
    })
  });
});
Enter fullscreen mode Exit fullscreen mode

This tangled mess of code and .then statements that we see can be easily improved using this syntax:

async function fetchUserAndGroups() {
  const user = await fetch('/users/lukeskw');
  const groups = await fetch(`/groups/${user.id}`);

  groups.map(group => {
    const groupInfo = await fetch(`/group/${group.id}`);
    console.log(groupInfo);
  });
}
Enter fullscreen mode Exit fullscreen mode

Each time we define an await, we are indicating to our interpreter to wait for the next Promise to execute and return a result before proceeding. This way, we prevent the subsequent lines from executing without the necessary variables.

It's worth remembering that every function we define with async automatically becomes a Promise. This means that we can attach asynchronous functions to each other. Let's see how this looks in code:

async function fetchUser() {
  const response = await fetch('/users/lukeskw');

  return response;
}

async function fetchGroups() {
  const user = await fetchUser();

  const response = await fetch(`/groups/${user.id}`);

  console.log(response);
}
Enter fullscreen mode Exit fullscreen mode

And what about the .catch?

So far, we have seen how to capture the result of the previous .then and assign it to a variable using await. But how do we handle errors? With this syntax, we can use the good ol' try/catch to ensure that errors in our code and Promise responses don't leave traces for the end user. Let's see how this works:

// Promise syntax (ES6)

function fetchUser() {
  fetch('/users/lukeskw')
    .then(response => console.log(response));
    .catch(err => console.log('Error:', err));
}

// New syntax (ES8+)

async function fetchUser() {
  try {
    const response = await fetch('/users/lukeskw');

    console.log(response);
  } catch (err) {
    console.log('Error:', err);
  }
}
Enter fullscreen mode Exit fullscreen mode

In addition to making the code more elegant, a great utility is to use a single catch for multiple Promises. This way, if the errors from these Promises have the same outcome for the end user, we can handle them in one place (while still separating them into multiple try/catch blocks if the errors are different).

And when to use it?

That's a tough question to answer, but for me, there is no restrictions to using async/await. In fact, it's been a while since I wrote a .then because I believe that the new syntax makes the code cleaner.

Conclusion

Async/await simplifies error handling with the familiar try/catch blocks, allowing us to centralize error handling logic and provide a better user experience. It also enhances code readability by reducing promise hell and minimizing nested structures.

Whether you're fetching data from an API, performing database queries, or handling any asynchronous task, async/await is a valuable tool in your JavaScript arsenal. Its adoption has become increasingly widespread, and it has quickly become the preferred approach for handling asynchronous operations.

So no more need of writing cascades of .then and .catch that we were used to 😁

. . . . . .