Parallel execution
Remember our lovely dice roll function? In the unlikely case you don't, here it is:
function roll() { return new Promise((resolve) => { setTimeout(() => { resolve(Math.floor(Math.random() * 5)); }, 500); }); } roll().then((number) => { console.log(`You just rolled a ${number}`); });
Toggle Console Output
Once again, that's a lot of bracket soup for just getting a number! But all the ingredients are needed:
- the outer
rollfunction allows us to deal with independent promise objects; - the callback passed to the
Promiseconstructor is needed to accessrejectandresolve; - the callback passed to
setTimeouttells JavaScript what to do after 500 milliseconds; - finally, the
Mathoperations that are directly passed toresolveas argument.
This is for what concerns the contents of the roll function. We call it then without arguments (roll()), and since such call returns a Promise object we call .then() on it. We pass an arrow function to then(), so we an access the value we resolved the Promise with, and eventually we log it to the console.
What a journey! Explaining code this way will make you a great developer because it shows you are in full control of the code and can explain every single symbol you have written. Dominance comes with a lot of practice so you'd better start right now.
The two dice problem
In order to get the result of two rolls, we had to nest two nested calls. We made it in CPS, let's do it with Promises:
function roll() { return new Promise((resolve) => { setTimeout(() => { resolve(Math.floor(Math.random() * 5)); }, 500); }); } roll().then(first => { roll().then(second => { console.log(`You rolled a ${first} + ${second} = ${first + second}`); }); });
Toggle Console Output
It's not the end of the world. But what about rolling three or more times? First, you have to come up with different parameter names for each .then() because they'll shadow each other if you call them all number. Second, you'll recreate the indention typical of callback hell.
Luckily, the Promise object exposes the static Promise.all method, that accepts an array of Promises and return a single Promise resolved when all the promises are fulfilled. It's easier done that said:
function roll() { return new Promise((resolve) => { setTimeout(() => { resolve(Math.floor(Math.random() * 5)); }, 500); }); } Promise.all([ roll(), roll(), roll(), ]).then(results => { console.log(results); });
Toggle Console Output
Awesome! Let's put some logs to understand better what's going on:
function roll() { console.log('rolling...') return new Promise((resolve) => { setTimeout(() => { const result = Math.floor(Math.random() * 5); console.log(`rolled a ${result}`); resolve(result); }, 500); }); } console.log('before'); Promise.all([ roll(), roll(), roll(), ]).then(results => { console.log(results); }); console.log('after');
Toggle Console Output
- first of all we get the
beforelog; - then three
rolling..., because it's at the beginning of therollfunction body; - then the
afterbecause JavaScript moves and does not wait for the timeout; - then the three
rolled awith the roll result; - eventually the array with the results inside the
.then()returned fromPromise.all.
The naming of promise things is quite fortunate:
When
allthe promises are ready,thendo something with the results.
Observe how the rolls are run in parallel, so the whole operation takes half a second, and not one second and a half. The rolls don't depend on each other, so nesting them would be suboptimal. With Promise.all we can both run things at the same time and know when all of them are done.
Dealing with errors
Let's now go back with our divide function, that throws an error in case it receives 0 as second argument. Let's run a batch of unproblematic divisions first:
function divide(a, b) { return new Promise((resolve, reject) => { if (b === 0) { reject(new Error('Cannot divide by zero!')); return; } setTimeout(() => { resolve(a / b); }, 500); }); } Promise.all([ divide(1, 2), divide(6, 3), divide(0, 1), ]).then(results => { console.log(results); });
Toggle Console Output
Cool. What about dividing by zero somewhere? Let's put some logs around:
function divide(a, b) { console.log(`calculating ${a} / ${b}`); return new Promise((resolve, reject) => { if (b === 0) { reject(new Error('Cannot divide by zero!')); return; } setTimeout(() => { const result = a / b; console.log(`${a} / ${b} = ${result}`); resolve(result); }, 500); }); } Promise.all([ divide(1, 2), divide(6, 0), divide(0, 1), ]).then(results => { console.log(results); }).catch(error => { console.log(error); });
Toggle Console Output
First we get the three calculating... logs. Then immediately the log inside the .catch(), because the error is thrown before the timeout. The other two timeout resolve successfully (hence the two additional calculation logs), but the Promise.all(...).then() is not called.
This is because the global Promise resolves when and if all the passed Promises are fulfilled. If any of them is rejected, the catch() path is taken.
How picky from Promise.all! Couldn't it close an eye if just a Promise fails? Well imagine that you have to upload an important document of ten pages, and the one with your signature is missing. You'd really want to be notified and to have the chance to upload it again.
Promise offers more relaxed options like:
Promise.alSettled: resolves when all the passed Promises are settled (i.e. fulfilled or rejected);Promise.race: resolves as soon as one of the passed Promises resolves;Promise.any: resolves if any of the passed Promises resolves;
But Promise.all is the strictest, and rightfully so!
An homemade implementation of Promise.all
It's a great exercise to reverse engineer standard features. Let's write a function that takes an array of Promises as parameter, and returns a Promise that resolves when all the passed Promises resolve, exactly like Promise.all.
Let's sketch the function first. We'll go back to rolls this time:
function roll() { return new Promise((resolve) => { setTimeout(() => { const result = Math.floor(Math.random() * 5); console.log(`Rolled a: ${result}`); resolve(result); }, 500); }); } function all(promises) { return new Promise((resolve) => { }); } all([ roll(), roll(), ]).then(results => { console.log(results); });
Toggle Console Output
We get the two rolled a: logs, but no .then(). This is because the Promise we return from all() never resolves! The resolve parameter is declared but never called.
The problem is to know when all the Promises are resolved. We know how many of them do we have, and we know how to populate an empty array. Let's try something:
function roll() { return new Promise((resolve) => { setTimeout(() => { const result = Math.floor(Math.random() * 5); console.log(`Rolled a: ${result}`); resolve(result); }, 500); }); } function all(promises) { return new Promise((resolve) => { const results = []; promises.forEach(p => { p.then(number => { results.push(number); console.log(`inside all: [${results}]`); }); }); }); } all([ roll(), roll(), ]).then(results => { console.log(results); });
Toggle Console Output
Never mind what the actual rolls are, you'll see the results array contents growing, first with one entry, then with two. We can compare the length of the array with the one of the promises parameter to decide when it's time to call resolve:
function roll() { return new Promise((resolve) => { setTimeout(() => { const result = Math.floor(Math.random() * 5); console.log(`Rolled a: ${result}`); resolve(result); }, 500); }); } function all(promises) { return new Promise((resolve) => { const results = []; promises.forEach(p => { p.then(number => { results.push(number); console.log(`inside all: [${results}]`); if (results.length === promises.length) { resolve(results); } }); }); }); } all([ roll(), roll(), ]).then(results => { console.log(results); });
Toggle Console Output
Ta daaan! Let's clean up the logs:
function roll() { return new Promise((resolve) => { setTimeout(() => { resolve(Math.floor(Math.random() * 5)); }, 500); }); } function all(promises) { return new Promise((resolve) => { const results = []; promises.forEach(p => { p.then(number => { results.push(number); if (results.length === promises.length) { resolve(results); } }); }); }); } all([ roll(), roll(), ]).then(results => { console.log(results); });
Toggle Console Output
That's awesome, it works exactly like Promise.all! Now don't stop at the top and implement yourself the catch case with the help of the divide function. You'll be ace of spades!
Conclusion
We now know how to wait for parallel async function execution before proceeding. This reduces the code complexity to one single then() level, and it's already a great achievement. In the next chapter we'll improve this even further by making finally acquaintance with the async / await keywords.
I cannot stress this enough: you don't master this topic if you don't rewrite these examples from scratch numerous times, even better if explaining them to somebody in the process. Merely following the lesson will bring you just that far. It's also unreasonable that you master it after a single session, never mind how thorough it can be. You'll need to go back to previous concepts various times in order to congregate your knowledge.
So code a lot, be in control of your explaining / typing rhythm and most important: have a lot of fun!