How to do Parallelization Right with Promise.all
Published on
-/- lines long
Parallelization is one of the easiest wins you can achieve in terms of performance. However, JavaScript gives you a chance to mess it up and even TypeScript doesn't save you as well.
Take the following code, for example:
const start = Date.now()
function delay<T>(time: number, value: T) {
return new Promise<T>((resolve) =>
setTimeout(() => {
console.log('resolved:', value, `${Date.now() - start}ms`)
resolve(value)
}, time)
)
}
const [val1, val2, val3] = await Promise.all([
await delay(100, 'foo'),
await delay(150, 'bar'),
await delay(200, 'baz'),
])
console.log(`all values: ${Date.now() - start}ms`)
This is perfectly valid TypeScript. But what does it output? It outputs this:
resolved: foo 101ms
resolved: bar 253ms
resolved: baz 455ms
all values: 455ms
There's no parallelization going on and the Promise.all call does absolutely nothing. This obviously happens because we have await in front of every argument, meaning that:
-
We wait 100ms for the first
delaycall -
Then we wait another 150ms for the second call
-
Then we wait 200ms more for the third call
-
Finally, we call
Promise.allwith the resolved arguments
Although this isn't what we want, it's perfectly valid. The correctly parallelized code is this:
const [val1, val2, val3] = await Promise.all([
delay(100, 'foo'),
delay(150, 'bar'),
delay(200, 'baz'),
])
Without the await keywords in front of each delay call, we get parallelization, as expected:
resolved: foo 101ms
resolved: bar 150ms
resolved: baz 201ms
all values: 201ms
This simple bug was making our code over 2 times slower. Have you made the same mistake yourself? Go check. And to avoid it in the future, use this wrapper:
function parallelize<T extends unknown[]>(promises: {
[K in keyof T]: Promise<T[K]>
}) {
return Promise.all(promises)
}
This simple wrapper around Promise.all makes sure that passing non-Promise values to it causes an error and doesn't go unnoticed. So if we have an accidental await, we'll know it:

Such a mistake is obvious in this minimal example here. But in the real world, where you're dealing with actual legacy production code that is all messy and twisted, an easy win like this could easily go unnoticed. So you can switch all your Promise.all calls for this parallelize wrapper and quickly find where you're wasting CPU cycles.