JavaScript Async Await: A helpful overview. I promise!
JavaScript async await is an important tool in the toolbox of every JavaScript Developer! You will need it as soon as you start working with APIs or other applications that require you to wait until you retrieved the data you need to continue.
To better understand async await, we will first lay an important foundation by looking at the event loop and promises. That knowledge will help you a lot. I promise!
The JavaScript Event Loop
Browser JavaScript and Node.js are both single-threaded. That means they can not use another thread to execute your asynchronous code. Instead, JavaScript uses the so-called event loop. You can imagine this event loop like a circle that runs repeatedly.
In the first run-through, all your synchronous code is executed, and asynchronous calls are queued up for later execution.
We got two examples for asynchronous tasks: microtasks (e.g., Promise) and macrotasks (e.g., setTimeout). When microtasks are fulfilled, they get called at the end of the event loop. Macrotasks, on the other hand, are called at the beginning of a new loop.
The following code example visualized that well!
Need help or want to share feedback? Join my discord community!
console.log(1);
setTimeout(_ => console.log(2), 0);
Promise.resolve().then(_ => console.log(3));
console.log(4);
/* console
1
4
3
2
*/
The code looks like it would call one line after the other because setTimeout
waits 0 seconds, and the Promise
resolves immediately. But as we learned before, different tasks get executed at a different point in time.
The following two images will visualize when we execute what in this example:
If this guide is helpful to you and you like what I do, please support me with a coffee!
What is a Promise?
A Promise
is an object that represents the execution of an asynchronous function. It can have one of the three following states:
- pending: Promise not finished
- fulfilled: Promised finished with a value
- rejected: Promise finished with an error
Now, let’s work with some examples to grasp better how promises work.
Work with a Promise
First, we will look at how to work with a Promise
, and later, we will create our own!
For example, fetch
sends a request to a website or API and returns a Promise
.
We can work with the returned Promise
by using the function .then
with a callback (learn more about callback functions in this post here) to determine what to do with the returned value when the Promise
is fulfilled.
const promise = fetch('https://randomuser.me/api/');
promise.then(res => res.json());
Because res.json()
is also a Promise
and we return it through our callback, we can append another .then
to the chain:
const promise = fetch('https://randomuser.me/api/');
promise
.then(res => res.json())
.then(data => console.log(data.results[0].name.first));
That way, we can also easily check for errors in all Promises in a chain. To do it, we have to append the .catch
function at the end of the chain. If one of our Promises throws an error, the Promises after it are automatically skipped, and the callback inside of catch
will execute.
const promise = fetch('https://randomuser.me/api/');
promise
.then(res => res.json())
.then(user => {
throw new Error('Something went wrong!');
return user;
})
.then(user => console.log(user.name))
.catch(err => console.error(err));
/* console
Error: Something went wrong!
*/
Create a Promise
Now we will not only use a Promise
but create our own. We will use an example where we create a piece of code that needs a long time to execute completely. That piece of code will return a Promise
so that we can move it in the background!
But first, let us create the code without a Promise
to see how long it takes to execute. To log the time, we will use the following log function and a timer called ‘Blocker’:
function log(text) {
console.log(text);
console.timeLog('Blocker');
}
Now, let’s execute the code:
console.time('Blocker');
log(1);
log(codeBlocker(2));
log(3);
function codeBlocker(text) {
let i = 0;
while (i < 100000000) {i++}
return text;
}
/* console
1
Blocker: 0.06103515625 ms
2
Blocker: 33.2568359375 ms
3
Blocker: 33.291748046875 ms
*/
Now we can see that the codeBlocker
function blocks the code for multiple milliseconds and then continues the execution (to know the time we used console function. To learn more about the different console functions, check here).
We do not want this behavior. Instead, we want the code to continue executing without a block. For that, we need to return a Promise
from our codeBlocker
function.
There you can fall into a pitfall. Doing it like this will still block the execution of the synchronous code!
console.time('Blocker');
log(1);
codeBlocker(2).then(val => log(val));
log(3);
function codeBlocker(text) {
return new Promise((resolve, reject) => {
let i = 0;
while (i < 100000000) {i++}
resolve(text);
})
}
/*
1
Blocker: 0.066162109375 ms
3
Blocker: 34.72216796875 ms
2
Blocker: 34.819091796875 ms
*/
To make it non-blocking, you would have to do it like this:
console.time('Blocker');
log(1);
codeBlocker(2).then(val => log(val));
log(3);
function codeBlocker(text) {
return Promise.resolve().then(v => {
let i = 0;
while (i < 100000000) {i++}
return text;
})
}
/* console
1
Blocker: 0.06005859375 ms
3
Blocker: 0.14013671875 ms
2
Blocker: 35.4462890625 ms
/*
As we can see, the log of 3 happens immediately after logging the value 1. Instead, 2 is moved to the background and logged as soon as the Promise
fulfills.
What is JavaScript async await?
Promises are already a nice way to work with asynchronous code. The only problem is that if you chain a lot of promises together, this can get unreadable. Therefore JavaScript introduced async await as a new syntax (syntactic sugar) for Promises. As promised, we laid the foundation for async await in the last section and should be good to go now!
So let us have a look at what both keywords do exactly. First of all, the keyword async
. This keyword transforms the return value of every function into a Promise.resolve(val)
(careful, this function is not moved to the background, it is more like our first Promise example and blocks the synchronous execution of the code!):
async function getComponenet(name) {
const components = { 'cpu': 'AMD Ryzen 5', 'ram': '16GB', ssd: '512GB' }
return components[name]
}
That is a huge improvement! We simplified the code by a lot by just adding the async
keyword!
In addition to easing the process of writing a Promise
, it also creates a scope where we can use the await
keyword. This keyword will allow us to pause the async
function’s execution, wait for the result of a Promise
, and use the resolved value further (to see what await
does, we will add a 1 sec delay to getComponent
).
async function delay(time) {
return new Promise(resolve => setTimeout(resolve, time));
}
async function getComponent(name) {
const components = { 'cpu': 'AMD Ryzen 5', 'ram': '16GB', ssd: '512GB' };
await delay(1000);
return components[name];
}
async function buildPc() {
let c1 = await getComponent('cpu');
log(c1);
let c2 = await getComponent('ram');
log(c2);
return [c1, c2];
}
console.time('Blocker');
log(await buildPc());
/* console
AMD Ryzen 5
Blocker: 1003.236083984375 ms
16GB
Blocker: 2018.936767578125 ms
['AMD Ryzen 5', '16GB']
Blocker: 2019.476806640625 ms
*/
As we can see, our code pauses to wait for functions with the await
keyword. But in the code above, there is still one “mistake”. In the case of this function, we should not wait for c1
before we start executing c2
. Instead, we should run them simultaneously to save time and make our code more efficient.
We only need to wait for one thing after the other if they are dependent on each other. In our case, we can let both functions run at the same time and wait for the results at the end of the function:
async function buildPc() {
let c1 = getComponent('cpu');
log(c1);
let c2 = getComponent('ram');
log(c2);
return Promise.all([c1, c2]);
}
console.time('Blocker');
log(await buildPc());
/* console
Promise {<pending>}
Blocker: 0.168212890625 ms
Promise {<pending>}
Blocker: 0.27392578125 ms
['AMD Ryzen 5', '16GB']
Blocker: 1005.56591796875 ms
*/
Now we can see that both functions return a Promise
and run simultaneously. So in the end, Promise.all()
waits for the results of both Promises and then returns them.
From this example, we learned that we should never stop our function if it is not necessary and that we can await multiple Promises by adding them into an array and await that:
await Promise.all([a,b]);
Important to note is that you can always use an array, also if you do not need the array resolved by this Promise!
Top-Level Await
You can use await outside of async functions in the Browser because it supports top-level await. In Node.js, on the other hand, you can use top-level await only inside of module scripts. You create them by either naming a file *.mjs or changing the type to module inside the package.json.
Error Handling with Javascript async await
In async await, you have a more granular error handling than with a Promise chain. You create a try-catch block around the async function you are awaiting and handle the error inside of the catch block. In case you haven’t heard about try and catch, check out this post on error handling.
To show how it works, we will use the buildPc
function of our last example.
async function buildPc() {
let c1 = getComponent('cpu');
let c2 = getComponent('ram');
throw "component broke";
return Promise.all([c1, c2]);
}
try {
await buildPc();
}
catch (err) {
console.error(err);
}
This way, we will catch the error thrown inside the buildPc
function, cancel the code’s execution inside the try block, and then log the caught error.
Conclusion
That was a lot. First, let that sink in, and then practice with JavaScript async await.
In this post, we learned about Promises, what they have to do with async await and how to use them. Then we created our first own Promise and used this knowledge to understand async await.
In short, async await is a more concise way of writing Promises. In addition, it makes creating asynchronous code more like writing synchronous code!
I hope this post was helpful for you, and you can do your first steps with async await now! In case you liked it, subscribe to my newsletter!
[convertkit form=2303042]