Tips For Using Async/Await - Write Better JavaScript!
Milind Soorya / June 12, 2021
7 min read
There’s a special syntax you can use in JavaScript that makes working with promises easier. It's called “async/await", and it’s surprisingly straightforward to understand and use.
In this article, we'll discuss:
- What are asynchronus functions?
- How promises work in JavaScript
- Async/Await basics
- How to use async/await with error handling
- How an async function returns a promise
- How to use promise.all()
So let's dive in.
What are asynchronus functions?
The term asynchronus refers to a situation where two or more events don't happen at the same time. Or in simple terms, multiple related things can happen without waiting for the previous action to complete.
In JavaScript, asynchronus functions are really important because of the single threaded nature of JavaScript. With the help of asynchrnous functions, JavaScript's event loop can take care of other things when the function is requesting some other resource.
You'd use aysnchronous code, for example, in API's that fetch a file from the network, when you're accessing a database and returning data from it, when you're accessing a video stream from a web cam, or if you're broadcasting the display to a VR headset.
How Promises Work in JavaScript
The Promise
object in JavaScript represents an asynchronous operation (and its resulting value) that will eventually complete (or fail).
A P
romise` can be in one of these states:
- pending: initial state, neither fulfilled nor rejected.
- fulfilled: meaning that the operation was completed successfully.
- rejected: meaning that the operation failed.
The function passed to a new promise is called the executor. It's arguments (resolve
and reject
) are called callbacks that JavaScript itself provides. When the executor gets the result, whether it's now or later it doesn’t matter – it should call one of these callbacks.
Here's an example of a Promise:
const myPromise = new Promise(function(resolve, reject) => {
setTimeout(() => {
resolve('foo');
}, 300);
});
And here are examples of a fulfilled vs a rejected promise:
// fulfilled promise
let promise = new Promise(function (resolve, reject) {
setTimeout(() => resolve(new Error('done!')), 1000);
});
// resolve runs the first function in .then
promise.then(
(result) => alert(result), // shows "done!" after 1 second
(error) => alert(error) // doesn't run
);
// rejected promise
let promise = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('Whoops!')), 1000);
});
// reject runs the second function in .then
promise.then(
(result) => alert(result), // doesn't run
(error) => alert(error) // shows "Error: Whoops!" after 1 second
);
Async/Await Basics in JavaScript
Things to note -
There are two parts to using async/await
in your code.
First of all, we have the async
keyword, which you put in front of a function declaration to turn it into an async function. An async function is a function that knows how to expect the possibility of the await
keyword being used to invoke asynchronous code.
The
async
keyword is added to functions to tell them to return a promise rather than directly returning the value.
const loadData = async () => {
const url = 'https://jsonplaceholder.typicode.com/todos/1';
const res = await fetch(url);
const data = await res.json();
console.log(data);
};
loadData();
// Console output
{
completed: false,
id: 1,
title: "delectus aut autem",
userId: 1
}
How to Use Async/Await with Error Handling
We can handle errors using a try catch block.
const loadData = async () => {
try {
const url = 'https://jsonplaceholder.typicode.com/todos/1';
const res = await fetch(url);
const data = await res.json();
console.log(data);
} catch (err) {
console.log(err);
}
};
loadData();
The above try-catch will only handle errors while fetching data such as the wrong syntax, wrong domain names, network errors, and so on.
When you want to handle an error message from the API response code, you can use res.ok
(res
is the varaiable to which response is stored). It will give you a Boolean with the value true if the response code is between 200 and 209.
const loadData = async () => {
try {
const url = 'https://jsonplaceholder.typicode.com/todos/qwe1';
const res = await fetch(url);
if (res.ok) {
const data = await res.json();
console.log(data);
} else {
console.log(res.status); // 404
}
} catch (err) {
console.log(err);
}
};
loadData();
// OUTPUT
// 404
How an Async Function Returns a Promise
This is one of the traits of async functions — their return values are guaranteed to be converted to promises. To handle data returned from an async
function we can use a then
keyword to get the data.
const loadData = async () => {
try {
const url = 'https://jsonplaceholder.typicode.com/todos/1';
const res = await fetch(url);
const data = await res.json();
return data;
} catch (err) {
console.log(err);
}
};
const data = loadData().then((data) => console.log(data));
💡 PRO TIP :
if you want to use an async-await
to handle the returned data you can make use of an IIFE, but it is only available in Node 14.8 or higher.
// use an async IIFE
(async () => {
const data = await loadData();
console.log(data);
})();
await only works inside async functions within regular JavaScript code, however it can be used on its own with JavaScript modules.
How to Use Promise.all() in JavaScript
Promise.all()
comes in handy when we want to call multiple API's. Using a traditional await
method we have to wait for each request to be completed before going to the next request. The problem occurs when each request takes some time to complete, this can easily add up and make the experience slower. Using Promise.all()
we can call each of these API's in parallel. (it is an oversimplification, for more details checkout this amazing article).
Please be carefull when using
Promise.all
, though – if one of the await requests fails, it will fail altogether.
const loadData = async () => {
try {
const url1 = 'https://jsonplaceholder.typicode.com/todos/1';
const url2 = 'https://jsonplaceholder.typicode.com/todos/2';
const url3 = 'https://jsonplaceholder.typicode.com/todos/3';
const results = await Promise.all([fetch(url1), fetch(url2), fetch(url3)]);
const dataPromises = await results.map((result) => result.json());
const finalData = Promise.all(dataPromises);
return finalData;
} catch (err) {
console.log(err);
}
};
const data = loadData().then((data) => console.log(data));
// Console output
[
{
completed: false,
id: 1,
title: 'delectus aut autem',
userId: 1
},
{
completed: false,
id: 2,
title: 'quis ut nam facilis et officia qui',
userId: 1
},
{
completed: false,
id: 3,
title: 'fugiat veniam minus',
userId: 1
}
];
Conclusion
In most situations we can use async/await
with a try catch
block to handle both results and errors.
At the moment, await
won’t work in top-level code. This means that when we’re outside any async
function, we’re syntactically unable to use await
. In this case, it’s regular practice to add .then/catch
to handle the final result or falling-through error.
Top level
await
is available from Node.jsv14.8
onwards and in ES modules only. Read more here: Top-level await is available in Node.js modules | Stefan Judis Web Development
Resources that helped me
- Promises (javascript.info)
- Introducing asynchronous JavaScript - Learn web development | MDN (mozilla.org)
- Promise - JavaScript | MDN (mozilla.org)