Asynchronous code is a regular part of JavaScript. As web apps get more complex, there’ll be more need for asynchronous code since JavaScript is single-threaded, and asynchronous code prevents the blocking of the main thread.
In this article, we’ll look at why async
and await
is the way to go for writing asynchronous code.
The Old Way to Write Promise Code
Before we look at async
and await
, we have to look at what it’s like in the old days.
In the olden days, we chain promises by writing something like the following:
Promise.resolve(1)
.then(val => Promise.resolve(2))
.then(val => Promise.resolve(3))
.then(val => Promise.resolve(4))
.then(val => Promise.resolve(5))
.then(val => Promise.resolve(6))
As we can see, we have to call then
a lot and in each then
call, we have to pass in a callback function to return the next promise. This is a pain because it’s very long-winded. If we want to do something like gathering the resolved values from each promise, then the code gets even longer.
For example, to gather the resolved values into an array, we have to write something like:
let vals = [];
Promise.resolve(1)
.then(val => {
vals.push(val);
return Promise.resolve(2)
})
.then(val => {
vals.push(val);
return Promise.resolve(3)
})
.then(val => {
vals.push(val);
return Promise.resolve(4)
})
.then(val => {
vals.push(val);
return Promise.resolve(5)
})
.then(val => {
vals.push(val);
return Promise.resolve(6)
})
.then(val => {
vals.push(val);
console.log(vals)
});
Then we get [1, 2, 3, 4, 5, 6]
as the value of vals
.
As we can see, the number of lines of code exploded compared to the first example.
There’s lots of repetition and takes up lots of space in a file.
To deal with this, it’s time to move to the present with the async-await
syntax for chaining promises.
Async and Await
To clean up the code in the example above, we can use async
and await
to do this.
We start a function declaration with async
and we use the await
keyword inside. await
does the same thing as the then
callbacks. It takes the resolved value of the promise and lets us do stuff with it.
With async
and await
, we can turn:
let vals = [];
Promise.resolve(1)
.then(val => {
vals.push(val);
return Promise.resolve(2)
})
.then(val => {
vals.push(val);
return Promise.resolve(3)
})
.then(val => {
vals.push(val);
return Promise.resolve(4)
})
.then(val => {
vals.push(val);
return Promise.resolve(5)
})
.then(val => {
vals.push(val);
return Promise.resolve(6)
})
.then(val => {
vals.push(val);
console.log(vals)
});
into:
(async () => {
const val = await Promise.resolve(1);
const val2 = await Promise.resolve(2);
const val3 = await Promise.resolve(3);
const val4 = await Promise.resolve(4);
const val5 = await Promise.resolve(5);
const val6 = await Promise.resolve(6);
const vals = [val, val2, val3, val4, val5, val6];
console.log(vals);
})();
The await
in the code above indicates that the code to the right of it returns a promise, and it’ll wait for the promise to be fulfilled until it moves on to the next one. Also, the resolved value can be assigned with the =
and the variable or constant to the left of it.
The values can be gathered into an array at the end of the function and we can log it.
Even though this looks like synchronous code, async
function can only return promises, so if we return vals
, we’ll get a promise that resolves to the value of vals
.
This means that:
const getVals = () => {
let vals = [];
Promise.resolve(1)
.then(val => {
vals.push(val);
return Promise.resolve(2)
})
.then(val => {
vals.push(val);
return Promise.resolve(3)
})
.then(val => {
vals.push(val);
return Promise.resolve(4)
})
.then(val => {
vals.push(val);
return Promise.resolve(5)
})
.then(val => {
vals.push(val);
return Promise.resolve(6)
})
.then(val => {
vals.push(val);
return Promise.resolve(vals);
});
}
is the same as:
const getValsAsyncAwait = async () => {
const val = await Promise.resolve(1);
const val2 = await Promise.resolve(2);
const val3 = await Promise.resolve(3);
const val4 = await Promise.resolve(4);
const val5 = await Promise.resolve(5);
const val6 = await Promise.resolve(6);
const vals = [val, val2, val3, val4, val5, val6];
return vals;
};
We can use await
on both and see what we get.
This block:
const getVals = () => {
let vals = [];
return Promise.resolve(1)
.then(val => {
vals.push(val);
return Promise.resolve(2)
})
.then(val => {
vals.push(val);
return Promise.resolve(3)
})
.then(val => {
vals.push(val);
return Promise.resolve(4)
})
.then(val => {
vals.push(val);
return Promise.resolve(5)
})
.then(val => {
vals.push(val);
return Promise.resolve(6)
})
.then(val => {
vals.push(val);
return Promise.resolve(vals);
});
}
(async () => {
const values = await getVals();
console.log(values);
})()
gets us [1, 2, 3, 4, 5, 6]
from the console.log
.
And:
const getValsAsyncAwait = async () => {
const val = await Promise.resolve(1);
const val2 = await Promise.resolve(2);
const val3 = await Promise.resolve(3);
const val4 = await Promise.resolve(4);
const val5 = await Promise.resolve(5);
const val6 = await Promise.resolve(6);
const vals = [val, val2, val3, val4, val5, val6];
return vals;
};
(async () => {
const values = await getValsAsyncAwait();
console.log(values);
})()
also gets us [1, 2, 3, 4, 5, 6]
from the console.log
. So they do exactly the same thing, just with much fewer lines of code.
Catching Errors
In the olden days we catch errors with the catch
and the callback passed into it.
For example, we write something like:
Promise.resolve(1)
.then(val => console.log(val))
.catch(err => console.log(err));
Now we can use try...catch
with async
and await
:
(async () => {
try {
const val = await Promise.resolve(1);
} catch (err) {
console.log(err);
}
})();
This doesn’t save much space, but this may still come in handy in case of errors.
As we can see, async
and await
make code for chaining promises so much shorter. It’s been available since 2017, so it’s supported by most modern browsers. The time to use this to clean up our code is definitely now.