How to add timeout to a Promise in Javascript

A reusable script to add timeout functionality to any async operation

Author's image
Tamás Sallai
5 mins

Promise timeouts

Promises in Javascript has no concept of time. When you use await or attach a function with .then(), it will wait until the Promise is either resolved or rejected. This is usually not a problem as most async tasks finish within a reasonable time and their result is needed.

But when a client is waiting for a response of, let's say, an HTTP server, it's better to return early with an error giving the caller a chance to retry rather than to wait for a potentially long time.

Fortunately, there is a built-in Promise helper function that can be used to add timeout functionality to any Promise-based construct.

Promise.race

The Promise.race is a global property of the Promise object. It gets an array of Promises and waits for the first one to finish. Whether the race is resolved or rejected depends on the winning member.

For example, the following code races two Promises. The second one resolves sooner, and the result of the other one is discarded:

const p1 = new Promise((res) => setTimeout(() => res("p1"), 1000));
const p2 = new Promise((res) => setTimeout(() => res("p2"), 500));

const result = await Promise.race([p1, p2]);
// result = p2

Similarly, it works for rejections also. If the winning Promise is rejected, the race is rejected:

const p1 = new Promise((res) => setTimeout(() => res("p1"), 1000));
const p2 = new Promise((_r, rej) => setTimeout(() => rej("p2"), 500));

try {
	const result = await Promise.race([p1, p2]);
} catch(e) {
	// e = p2
}

The arguments of the Promise.race function are Promises. This makes it work with async functions too:

const fn = async (time, label) => {
	await new Promise((res) => setTimeout(res, time));
	return label;
}

const result = await Promise.race([fn(1000, "p1"), fn(500, "p2")])
// result = p2

Just don't forget to call the async functions so that the race gets Promises. This can be a problem with anonymous functions and those need to be wrapped IIFE-style:

const result = await Promise.race([
	fn(1000, "p1"),
	(async () => {
		await new Promise((res) => setTimeout(res, 500));
		return "p2";
	})(),
]);
// result = p2

Timeout implementation

With Promise.race, it's easy to implement a timeout that supports any Promises. Along with the async task, start another Promise that rejects when the timeout is reached. Whichever finishes first (the original Promise or the timeout) will be the result.

const timeout = (prom, time) =>
	Promise.race([prom, new Promise((_r, rej) => setTimeout(rej, time))]);

With this helper function, wrap any Promise and it will reject if it does not produce a result in the specified time.

// resolves in 500 ms
const fn = async () => {
	await new Promise((res) => setTimeout(res, 500));
	return "p2";
}

// finishes before the timeout
const result = await timeout(fn(), 1000);
// result = p2

// timeouts in 100 ms
await timeout(fn(), 100);
// error

It's important to note that it does not terminate the Promise if the timeout is reached, just discard its result. If it consists of multiple steps, they will still run to completion eventually.

Clear timeout

The above solution uses a setTimeout call to schedule the rejection. Just as the original Promise does not terminate when the timeout is reached, the timeout Promise won't cancel this timer when the race is finished.

While this does not change how the resulting Promise works, it can cause side-effects. The event loop needs to check whether the timer is finished, and some environments might work differently if there are unfinished ones.

Let's make the wrapper function use Promise.finally to clear the timeout!

const timeout = (prom, time) => {
	let timer;
	return Promise.race([
		prom,
		new Promise((_r, rej) => timer = setTimeout(rej, time))
	]).finally(() => clearTimeout(timer));
}

The above implementation saves the setTimeout's result as timer and clears it when the race is over.

Error object

The race is over and there is a rejection. Was it because of the timeout or there was an error thrown from the Promise?

The above implementation does not distinguish between errors and this makes it hard to handle timeouts specifically.

const fn = async () => {
	throw new Error();
};

try {
	const result = await timeout(fn(), 1000);
}catch(e) {
	// error or timeout?
}

The solution is to add a third argument that is the timeout rejection value. This way there is an option to differentiate between errors:

const timeout = (prom, time, exception) => {
	let timer;
	return Promise.race([
		prom,
		new Promise((_r, rej) => timer = setTimeout(rej, time, exception))
	]).finally(() => clearTimeout(timer));
}

What should be a timeout error object?

Symbols in Javascript are unique objects that are only equal to themselves. This makes them perfect for this use-case. Pass a Symbol as the timeout error argument then check if the rejection is that Symbol.

const timeoutError = Symbol();
try {
	const result = await timeout(prom, 1000, timeoutError);
	// handle result
}catch (e) {
	if (e === timeoutError) {
		// handle timeout
	}else {
		// other error
		throw e;
	}
}

This construct allows handling the timeout error specifically.

November 20, 2020
In this article