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);
}
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);
});
})
});
});
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);
});
}
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);
}
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);
}
}
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 😁