I was presented with an interesting challenge recently. That challenge was to recreate a method provided by Javascript. Any guesses what that method was? That's right! Promise.all().
The purpose of the exercise was to get a deeper understanding of how promises in Javascript work. If you've worked with anything Promises
or Async/Await
before, then you know that there are specific things that have to happen in a certain way and order to work- and this is all by design.
Gathering Context
With that said, we can deduce a few things from Promise.all()
and use that to scope what our function needs to do.
What do we know about Promise.all()
? We know it accepts an array of promises. We know it returns an array of what those promises return. We know the returned array is itself a promise. We also know that it only does that last step if all promises succeed- so it errors if any of the passed in promises fail.
Our Definition of Done list could look something like this:
- It accepts an array of promises
- It returns a promise
- It returns an array of those promises' returned value
- It errors if any of the passed in promises fail
Jumping into the Code
We're only going to be writing a single function for this article. If you want to code along then you can pop open a new index.js file and use your terminal to test it once we're done.
Step one of this is to create the function. Since we're mimic-ing Promise.all()
, we can call this function promiseAll
// index.js
function promiseAll() {
}
Not too tough, huh? The next step is to let the function know to expect an array of promises when it is called.
// index.js
function promiseAll(promises) {
}
That's Definition of Done number one off of our list!
It accepts an array of promises- It returns a promise
- It returns an array of those promises' returned value
- It errors if any of the passed in promises fail
Next we want to set this function up to return a promise.
I highlighted those two words because they almost literally tell us what we need to do next. How do you make a function return? How do you create a new Promise?
If you can answer those two questions, then you already know what our next code snippet should look like.
// index.js
function promiseAll(promises) {
return new Promise((resolve, reject) => {
})
}
See what I mean? We had to return
a new Promise
. And that's item two of of our checklist
It accepts an array of promisesIt returns a promise- It returns an array of those promises' returned value
- It errors if any of the passed in promises fail
Returning an Array of Promises
Number 3 on our checklist is where the difficulty ramps up a bit.
Let's break down what we need.
We need:
- an array we can return
- to get the returned values of the promises passed in
Let's take that one step further. We know we're only going to return the promises' values in an array if they all return successfully.
Knowing that, lets create an array called successes
// index.js
function promiseAll(promises) {
return new Promise((resolve, reject) => {
let successes = [];
})
}
Now we need to somehow get the returned values of all promises passed in. Can you think of a couple of ways we can iterate through each promise?
We can use a for loop or the .map()
method. Either here would work, but I'm going to use the .map()
since I'm more familiar with it. Read up on .map() here
Let's map through our promises
// index.js
function promiseAll(promises) {
return new Promise((resolve, reject) => {
let successes = [];
promises.map((promise) => {
}
})
}
Now we can do promise stuff within the scope of each individual promise passed in.
What we'll be doing here is calling each promise individually and using a .then()
to then get access to its returned value.
We'll also want to add a .catch
to handle any errors. This actually checks off the fourth thing on our list.
// index.js
function promiseAll(promises) {
return new Promise((resolve, reject) => {
let successes = [];
promises.map((promise) => {
return promise.then((res) => {
}.catch((err) => {
reject(err)
}
}
})
}
Remember that our larger function is trying to return an array
of returned values. Knowing that, we shouldn't immediately resolve our promises.
Instead, we'll push our returned values to our successes array we created earlier.
// index.js
function promiseAll(promises) {
return new Promise((resolve, reject) => {
let successes = [];
promises.map((promise) => {
return promise.then((res) => {
successes.push(res)
}.catch((err) => {
reject(err)
}
}
})
}
We're getting close!!
Do you know what should happen next? Let's recap.
- Our function is returning a promise.
- Our function is set to error if any of the passed in promises fail.
- And we're pushing our returned values to a
successes
array.
So what's left? Now we need to resolve
our promise, but there's a condition with it.
We only want to resolve if all passed in promises succeed.
We can do that with an if statement by comparing the length of our successes array to the length of the promises passed in.
// index.js
function promiseAll(promises) {
return new Promise((resolve, reject) => {
let successes = [];
promises.map((promise) => {
return promise.then((res) => {
successes.push(res)
if(successes.length === promises.length) {
resolve(successes)
}
}.catch((err) => {
reject(err)
}
}
})
}
Now we're making promises we can keep!
Congratulations! You've just rebuilt the functionality of Promise.all()
and that's the last thing on our list!
It accepts an array of promisesIt returns a promiseIt returns an array of those promises' returned valueIt errors if any of the passed in promises fail
Running our Promise
Finishing up, let's run our function and see how it works.
Add these to the bottom of your index.js file.
const p1 = Promise.resolve(1);
const p2 = new Promise((resolve, reject) => setTimeout(() => resolve(2), 100));
promiseAll([p2, p1]).then((res) => console.log(res));
Now, in your terminal-- be sure you're in the right directory- and run node index.js
.
You should see the fruits of your labor console.logged before you!
Bonus
There's a small bug in this code. Can you see what it is?
Given the nature of promises, we can assume that there is a good chance that the promises passed in won't return in the same order as when they're passed in.
We're using .push()
to add our returned value to the successes array. This means that values will always be inserted to the end of the array, so if promise 2 returns before promise 1, it will actually show up in the first index of the successes array.
You can actually see this in our example now.
You would expect to see [2, 1]
since we passed the args in this order (P2, P1)
- but they're actually backwards! What we see in the terminal is [1, 2]
.
This is because P1 resolves immediately, whereas p2 returns a new promise and then resolves.
How would you fix this?
Hint: You could use the index of each passed in promise and then insert their returned value at that index of the successes array.
Top comments (11)
After reading the introduction, I felt like doing this myself before continuing with the rest of the article; here's my result:
However, this looks very ugly, and keeping track of results manually feels very hacky. So here's a second iteration:
This had me stumped for a while as to why the second iteration works — I was expecting the promises would run in serial so the time to completion would be longer. That isn't the case, because
Promise.all
takes an array of bare promises, not an array of functions resolving to promises.With the following tuples of milliseconds and resolved values:
Your second iteration runs as following:
Whether or not that's how
Promise.all
works under the hood, the end result is the same — all values, in the correct order, resolved in ~999 ms total time.Console test runner
Yes, this was quite unintuitive the first time I saw something like that, but you can just loop over threads, promises, etc. and wait for each one to finish, since you're ultimately waiting for the one that runs the longest anyway, and even if it comes first, the others will then just resolve instantly.
I think what makes it doubly confusing is that a very common pattern for
Promise.all
is mapping over an array, so the callback tomap
takes a function resolving to a promise, even though what's directly being passed toPromise.all
is the returned promises themselves.cb
is(url: String) => Promise<any>
, butpromises
isArray<Promise<any>>
, notArray<(url: String) => Promise<any>>
.To fetch and do something with each resolved object in series, you'd do this:
Time taken: total latency of all requests.
But you could equally loop over the promises instead of the urls:
Time taken: max latency of any one request.
Maybe this is just spelling out the obvious for some people but personally I still find it somewhat unintuitive, until you have that lightbulb moment 💡
Isn't that a problem for error-handling, though? If you're still awaiting the 999ms Promise and the 800ms one rejects with an error, what happens?
I love this solution
Bugfixed version:
Spoiler
We write directly to indexes in the array, instead of
push
ing, and keep track of the resolved count in a separate variable.Beautiful!!
Can be made a little more elegant at the cost of a little performance like this:
That way we can get rid of the
resolvedCount
variable.But I think @darkwiiplayer 's second solution is by far the most elegant 😉
Nicely done!
here's what I did as I followed along:
pretty much the same except I used
for - of
Siiiick!!