So recently, I needed to use Promise and async a lot. We're going to see how to program asynchronously.
Callbacks
Traditionally, ancient programmers used callback functions to perform asynchronous tasks. However, with this style, their code will be extremely unreadable. When you gaze long into the abyss. The abyss gazes also into you.
Promises
Let's see the following code:
const f1 = () =>
new Promise((resolve, reject) =>
setTimeout(() =>
console.log("f1"), resolve("f1"), 1000));
const f2 = () =>
new Promise((resolve, reject) =>
setTimeout(() =>
console.log("f2"), resolve("f2"), 1250));
const f3 = () =>
new Promise((resolve, reject) =>
setTimeout(() =>
console.log("f3"), resolve("f3"), 1500));
Promise.all([f2(), f3(), f1()]).then((result) => console.trace(result));
We can say that a Promise represents a possible future value. There may be some reasons for it to be rejected, but we don't know yet! In the above example, we use the method Promise.all() to perform several asynchronous tasks, in parallel. We immediately see something interesting. The first function called is f1 followed by f3, and finally f2, f1 takes $1000$ms to finish though. If we add a function to the code:
const f4 = () =>
new Promise((resolve, reject) =>
setTimeout(() =>
console.log("f4"), reject("f4"), 1000));
Promise.all([f2(), f3(), f1(), f4()]).then((result) => console.trace(result));
That wouldn't work. It gives you a list of warnings. After catching the rejection, we know that if there's any rejection, Promise.all() wouldn't work. We have to use Promise.allSettled(). Therefore, the code:
Promise.allSettled([f2(), f3(), f1(), f4()])
.then((result) => console.trace(result));
will give you
Trace: [
{ status: 'fulfilled', value: 'f2' },
{ status: 'fulfilled', value: 'f3' },
{ status: 'fulfilled', value: 'f1' },
{ status: 'rejected', reason: 'f4' }
]
which is very nice to work with. However, as of July 2020, if you are using Typescript, you will need to add es2020 of lib in tsconfig.json to enjoy this feature.
A sidenote
Sometimes, we want to get the first promise being rejected or resolved. We can use Promise.race(). This may be useful for requests to multiple identical sources, for example.
Async-Await
async and await are introduced in ES2017. It solves the problem of having a lot of callbacks in a chain, improving code readability. It seems with this style, erroneous pieces of code are easier to spot. An async function is a function that returns a promise. Suppose we need to write something like
const f1 = () =>
new Promise((resolve, reject) => {
setTimeout(() => {
console.log("First");
resolve("First");
}, 1000);
});
f1()
.then((result) => {
setTimeout(() => {
console.log(`Second ${result}`);
}, 1250);
return "Second";
})
.then((result) => {
setTimeout(() => {
console.log(`Third ${result}`);
}, 1500);
});
that gives you
First
Second First
Third Second
The following gives you the same result, but we get rid of all .then()s. They have similar sizes in bytecode as well.
const f1 = () =>
new Promise((resolve, reject) => {
setTimeout(() => {
console.log("First");
resolve("First");
}, 1000);
});
const f2 = async () => {
const result = await f1();
const secondResult = (() => {
setTimeout(() => {
console.log(`Second ${result}`);
}, 1250);
return "Second";
})(result);
(() => {
setTimeout(() => {
console.log(`Third ${secondResult}`);
}, 1500);
})(secondResult);
}
f2()
All we have to remember is that await always lives inside an async function. Otherwise, you will face a syntax error. For Promise.all(), the usage is similar. async and await are better in error handling (its clear syntax and its resultant error stacks) as well.