This article will be an example driven guide on writing promises, consuming them through async / await
code, and using them in loops.
As a note, this article assumes that you are already familiar with concepts such as Blocking vs Non-Blocking code in JavaScript and have maybe even seen (or written) a promise before.
If you are interested in more of a deep dive on promises, be sure to check out Mozilla’s official docs on promises on the MDN website.
The Example We Will Build Up To
For those who want to skip straight to the conclusion, here is the example that we are building up to:
// promisified 'setTimeout' function
function delay(delayedFunction, time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
resolve(delayedFunction());
} catch (error) {
reject(error.message);
}
}, time);
});
}
// top-level async declaration
async function main() {
// await using a for...of loop
const delaysInMs = [3000, 2000, 1000];
for (const time of delaysInMs) {
console.log(await delay(() => `print after ${time / 1000} second(s)`, time));
}
// await using a while loop
let count = 0;
const max = 100;
while (count < max) {
const result = await delay(() => `${++count} of ${max} lines...`, 1000);
console.log(result);
}
}
main();
A Classic Way of Delaying Code Execution
A great place to illustrate the benefits of Promises
in JavaScript is to fiddle with a classic built-in function called setTimeout
.
This function takes two parameters (the first being a function that you would like to execute, and the second being time in milliseconds) and delays the execution of the function by that amount of time:
setTimeout(inputFunction, timeInMilliseconds);
It is also common place for programmers to use an anonymous function as the inputFunction
:
console.log('1 | print immediately');
setTimeout(() => {
console.log('2 | print after one second');
}, 1000);
The following would output:
1 | print immediately
2 | print after one second
Where line 2
in fact would output into the console after one second.
Now lets say we wanted to print a third line which prints exactly one second after line 2
. We could go about it in two naive ways:
In one way, we manually change the time value to 2000
in the second invocation of setTimeout
to simulate line 3
executing one second after line 2
:
console.log('1 | print immediately');
setTimeout(() => {
console.log('2 | print after one second');
}, 1000);
setTimeout(() => {
console.log('3 | print one second after line 2');
}, 2000);
In another way, we nest the first setTimeout
with another setTimeout
which has the same time value:
console.log('1 | print immediately');
setTimeout(() => {
console.log('2 | print after one second');
setTimeout(() => {
console.log('3 | print one second after line 2');
}, 1000);
}, 1000);
Now you could imagine how messy this gets if we wanted to print 100 lines, all one second after another.
Lets promisify our beloved setTimeout
function and you’ll see how printing 100 lines one second after another won’t be all that difficult.
Declaring A Promise
A promise is not accessed or created using a reserved word, rather it is an object that must be instantiated using the built-in Promise
class. In the case of promisifying our setTimeout
function, we can declare a new function called delay
like so:
function delay(delayedFunction, time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(delayedFunction());
}, time);
});
}
Here our new delay
function returns a Promise
object. The promise takes a function
as its first parameter and captures the two values resolve
and reject
inside of it (which are also functions as we shall explore below).
So far we have kepts things simple, but lets assume that we execute code that can crash or fail, or throw some kind of error in our delayedFunction
, lets upgrade our example with some basic error handling and add try / catch
blocks. This will let us utilize reject(...)
:
function delay(delayedFunction, time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
resolve(delayedFunction());
} catch (error) {
reject(error.message);
}
}, time);
});
}
Now, we have a promise based delay
function which we can feed functions into. When we invoke delay
, we will followup by invoking a method made available by our promise object called .then(...)
, lets try it out:
console.log('1 | print immediately');
delay(() => '2 | print after one second', 1000)
.then(result => console.log(result));
Similar as in our previous example, this also outputs:
1 | print immediately
2 | print me after one second
With the exact timing that the logs imply.
The .then(...)
method runs using the SUCCESSFULLY returned results of the delayedFunction
. We can see them being fed into resolve(...)
. We also have another method called .catch(...)
that we can use to handle rejected promises.
console.log('1 | print immediately');
delay(() => {
throw new Error('This is a fake error.');
return '2 | print after one second';
}, 1000)
.then(result => console.log(result))
.catch(error => console.log(error));
Now this will output:
1 | print immediately
This is a fake error
Here we are intentionally throwing a new Error
with an error message of This is a fake error.
As you can see, we are able to catch it perfectly.
This still doesn’t fully address our problem though. If we wanted to execute a bunch of delay
functions one after the other, we still have to deal with the following:
console.log('1 | print immediately');
delay(() => '2 | print me after one second', 1000)
.then(result => {
console.log(result);
delay(() => '3 | print me one second after line 2', 1000)
.then(result => {
console.log(result);
/* ...CALLBACK HELL... */
});
});
This becomes really difficult to read as our task(s) grow in complexity (and is also known as CALLBACK HELL). Here is where async / await
comes in.
Async / Await
Long story short, if something returns a Promise
object, you can use async / await
with it and avoid callback hell. Let’s transform our previous example to illustrate the benefits.
First we have to declare a top-level async function, what this means is that we are letting JavaScript know that during the execution of a code block, we want to use the await
reserved word inside it.
function delay(delayedFunction, time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
resolve(delayedFunction());
} catch (error) {
reject(error.message);
}
}, time);
});
}
async function main() {
/* Now we can use 'await' in here with our `delay` function. */
}
main();
Now lets use await
to invoke our delay function in an appropriate fashion:
async function main() {
console.log('1 | print immediately');
console.log(await delay(() => '2 | print after one second', 1000));
}
main();
And we get our expected result of:
1 | print immediately
2 | print after one second
Now lets add quite a few more lines:
async function main() {
console.log('1 | print immediately');
console.log(await delay(() => '2 | print after one second', 1000));
console.log(await delay(() => '3 | print one second after line 2', 1000));
console.log(await delay(() => '4 | print one second after line 3', 1000));
console.log(await delay(() => '5 | print one second after line 4', 1000));
}
main();
And we get:
What await
is doing is replacing
delay(() => '[RETURN VALUE]', 1000)
.then(result => {
console.log(result); // [RETURN VALUE] in result
});
with
const result = await delay(() => '[RETURN VALUE]', 1000);
console.log(result); // [RETURN VALUE] in result
So we can transform the following:
delay(() => '[RETURN VALUE 1]', 1000)
.then(result => {
console.log(result); // [RETURN VALUE 1] in result
delay(() => '[RETURN VALUE 2]', 1000)
.then(result => {
console.log(result); // [RETURN VALUE 2] in result
});
});
into
let result;
result = await delay(() => '[RETURN VALUE 1]', 1000);
console.log(result); // [RETURN VALUE 1] in result
result = await delay(() => '[RETURN VALUE 2]', 1000);
console.log(result); // [RETURN VALUE 2] in result
Much cleaner isn’t it?
Async / Await in Loops
There may be multiple ways of combining async / await
with iteration, in this article we will explore just two basic ways:
Using a for loop
You can use await
very easily inside of a for ... of
loop:
async function main() {
const delaysInMs = [3000, 2000, 1000];
for (const time of delaysInMs) {
const result = await delay(() => `print after ${time / 1000} second(s)`, time);
console.log(result);
}
}
main();
Using a while loop
You can also use a simple while
loop:
async function main() {
let count = 0;
const max = 5;
while (count < max) {
const result = await delay(() => `${++count} of ${max} lines...`, 1000);
console.log(result);
}
}
main();
And there you have it. Thanks for reading and happy coding!