Write your own Promise.all()
Info
This post works in TypeScript and JavaScript. If you don't know, or don't care about TypeScript you can toggle it here , or in any of the code blocks in this post.
Promise.all()
is a super useful function that takes a list of promises and
returns a promise that resolves to the results of each of the promises. Most
importantly, it returns the results of your promises in the same order, and
runs the promises concurrently.
Getting started
To get started, we need a function that matches the signature of Promise.all
,
i.e. one that accepts a list of promises and returns a promise. We're using the
Promise
constructor here as it allows us to manually resolve the promise
whenever we want with its executor function (the function argument that you pass
to new Promise
). For now, we'll resolve the promise immediately with an empty
array and for the majority of the article we'll be working out of the executor
function.
type PromiseAllResult<Promises extends readonly unknown[] | []> = Promise<{
-readonly [K in keyof Promises]: Awaited<Promises[K]>
}>
const myPromiseAll = <Promises extends readonly unknown[] | []>(
promises: Promises
): PromiseAllResult<Promises> => {
return new Promise((resolve, reject) => {
resolve([] as unknown as Awaited<PromiseAllResult<Promises>>)
})
}
const myPromiseAll = promises => {
return new Promise((resolve, reject) => {
resolve([])
})
}
Explanation of complex types
The types above may seem overly complex, but they're required to make sure
types flow through the function. They make sure that the function's output
is correctly typed and are exactly the same as the real Promise.all
's types.
Here's my best attempt at an explanation of the more confusing parts:
- We need a generic so that the output types can match the input types. The
Promises
type argument extendsreadonly unknown[]
to ensure that the input is an array. We mark the array type asreadonly
as it allows the function's types to work with a wider variety of inputs. The additional| []
allows the types of tuple elements to flow through the function too. Finally, we useunknown
instead ofany
as it prevents us from accidentally making mistakes in the function body. - In the output type we use a mapped type
which allows for correct output types when using tuples. The
-readonly
in the mapped type makes the resulting type not readonly, andAwaited
unwraps promise types, converted something likePromise<number>
tonumber
.
Let's now extract out the results array so that we can easily set values in later steps:
// ...
const results = [] as unknown as Awaited<PromiseAllResult<Promises>>
resolve(results)
// ...
const results = []
resolve(results)
The naïve approach
A first implementation might look like this:
// ...
const results = [] as unknown as Awaited<PromiseAllResult<Promises>>
for (const promise of promises) {
promise.then(promiseResult => {
results.push(promiseResult as any)
})
}
resolve(results)
// ...
const results = []
for (const promise of promises) {
promise.then(promiseResult => {
results.push(promiseResult)
})
}
resolve(results)
This has a very big problem however. Promises are asynchronous, so the
function we pass to .then
will not be called immediately and calling resolve
right after the for-loop means the promise gets resolved with an empty array. In
the demo below you probably won't even see the Loading...
text due to how
immediately myPromiseAll
resolves.
Instead, we need some way of knowing when all the promises are complete and only
then resolve
-ing the promise.
Waiting for all the promises to finish
We can make sure all promises are complete by only resolve
-ing our promise
when the number of results we have equals the number of promises provided.
// ...
const results = [] as unknown as Awaited<PromiseAllResult<Promises>>
for (const promise of promises) {
promise.then(promiseResult => {
results.push(promiseResult as any)
if (results.length === promises.length) {
resolve(results)
}
})
}
// ...
const results = []
for (const promise of promises) {
promise.then(promiseResult => {
results.push(promiseResult)
if (results.length === promises.length) {
resolve(results)
}
})
}
... and now this works! We're waiting for all the promises to complete before
resolve
-ing, so all the results are available myPromiseAll
resolves.
However, we have one big problem: the results are sometimes in the wrong order:
Setting results in the right order
Making sure results end up in the right order requires a bit of a refactor - we'll need to:
- initialise the
results
array to be the same length as the inputpromises
array. - know the index of each promise so we can set its result in the correct position in the output array.
- separately keep track of how many promises have been resolved as point
1.
means thatresults.length
will always be the same aspromises.length
.
// ...
const results = Array(promises.length) as unknown as Awaited<
PromiseAllResult<Promises>
>
let numResolvedPromises = 0
for (let i = 0; i < promises.length; i++) {
const promise = promises[i]
promise.then(promiseResult => {
results[i] = promiseResult as any
numResolvedPromises++
if (numResolvedPromises === promises.length) {
resolve(results)
}
})
}
// ...
const results = Array(promises.length)
let numResolvedPromises = 0
for (let i = 0; i < promises.length; i++) {
const promise = promises[i]
promise.then(promiseResult => {
results[i] = promiseResult
numResolvedPromises++
if (numResolvedPromises === promises.length) {
resolve(results)
}
})
}
We can now see that the order of results of our myPromiseAll
function is
consistently the same as Promise.all
:
Handling errors
So far, we've handled everything going well, but we haven't handled any errors
yet. Promise.all
throws an error if any of the provided promises reject. We
can accomplish this by calling reject
with the error when any of the provided
promises throw:
// ...
const results = Array(promises.length) as unknown as Awaited<
PromiseAllResult<Promises>
>
let numResolvedPromises = 0
for (let i = 0; i < promises.length; i++) {
const promise = promises[i]
promise
.then(promiseResult => {
results[i] = promiseResult as any
numResolvedPromises++
if (numResolvedPromises === promises.length) {
resolve(results)
}
})
.catch((error: unknown) => {
reject(error)
})
}
// ...
const results = Array(promises.length)
let numResolvedPromises = 0
for (let i = 0; i < promises.length; i++) {
const promise = promises[i]
promise
.then(promiseResult => {
results[i] = promiseResult
numResolvedPromises++
if (numResolvedPromises === promises.length) {
resolve(results)
}
})
.catch(error => {
reject(error)
})
}
Now, this is fine - the Promise
constructor only accepts the first resolve
,
or reject
, and any other times the functions are called are ignored. However,
if you're like me, you'd like some extra confirmation that nothing goes wrong:
// ...
const results = Array(promises.length) as unknown as Awaited<
PromiseAllResult<Promises>
>
let numResolvedPromises = 0
let settled = false
for (let i = 0; i < promises.length; i++) {
const promise = promises[i]
promise
.then(promiseResult => {
if (settled) return
results[i] = promiseResult as any
numResolvedPromises++
if (numResolvedPromises === promises.length) {
settled = true
resolve(results)
}
})
.catch((error: unknown) => {
if (settled) return
settled = true
reject(error)
})
}
// ...
const results = Array(promises.length)
let numResolvedPromises = 0
let settled = false
for (let i = 0; i < promises.length; i++) {
const promise = promises[i]
promise
.then(promiseResult => {
if (settled) return
results[i] = promiseResult
numResolvedPromises++
if (numResolvedPromises === promises.length) {
settled = true
resolve(results)
}
})
.catch(error => {
if (settled) return
settled = true
reject(error)
})
}
Handling non-promises
We're now almost done. One last minor thing we need to do is handle when an
element in the promises array isn't a promise. When Promise.all
encounters a
value that isn't a promise, it still works while our myPromiseAll
so far would
throw an error. We can fix this by wrapping each item in a promise that resolves
immediately with Promise.resolve
:
// ...
for (let i = 0; i < promises.length; i++) {
const promise = Promise.resolve(promises[i])
// ...
}
// ...
for (let i = 0; i < promises.length; i++) {
const promise = Promise.resolve(promises[i])
// ...
}
All together
Here's the entire code for myPromiseAll
:
type PromiseAllResult<Promises extends readonly unknown[] | []> = Promise<{
-readonly [K in keyof Promises]: Awaited<Promises[K]>
}>
const myPromiseAll = <Promises extends readonly unknown[] | []>(
promises: Promises
): PromiseAllResult<Promises> => {
return new Promise((resolve, reject) => {
const results = Array(promises.length) as unknown as Awaited<
PromiseAllResult<Promises>
>
let numResolvedPromises = 0
let settled = false
for (let i = 0; i < promises.length; i++) {
const promise = Promise.resolve(promises[i])
promise
.then(promiseResult => {
if (settled) return
results[i] = promiseResult as any
numResolvedPromises++
if (numResolvedPromises === promises.length) {
settled = true
resolve(results)
}
})
.catch((error: unknown) => {
if (settled) return
settled = true
reject(error)
})
}
})
}
const myPromiseAll = promises => {
return new Promise((resolve, reject) => {
const results = Array(promises.length)
let numResolvedPromises = 0
let settled = false
for (let i = 0; i < promises.length; i++) {
const promise = Promise.resolve(promises[i])
promise
.then(promiseResult => {
if (settled) return
results[i] = promiseResult
numResolvedPromises++
if (numResolvedPromises === promises.length) {
settled = true
resolve(results)
}
})
.catch(error => {
if (settled) return
settled = true
reject(error)
})
}
})
}