Skip to Content

How to do Parallelization Right with Promise.all

Published on

Random characters surrounding "Promise.all" and "await"

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:

  1. We wait 100ms for the first delay call
  2. Then we wait another 150ms for the second call
  3. Then we wait 200ms more for the third call
  4. Finally, we call Promise.all with 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:

Cursor IDE TypeScript error tooltip

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.