JavaScript Async Await: A helpful overview. I promise!

No Comments

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!

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:

javascript async await: eventloop

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:

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!

Discussion (0)

Add Comment

Your email address will not be published.