Vladimir Klepov as a Coder

How to timeout a promise

Timeouts are one of the key building blocks to make your app stable. In short, if you send a request to an endpoint and a response does not, for whatever reason, come soon, we act as if the request failed and fall back to plan B — try again, show an error message and let the user decide what to do next, or use cached data. This is a great remedy for all kinds of flaky-web trouble: slow networks, clogged backends, overloaded databases — your user will never have to watch a spinner forever.

Fetch API has a signal option to abort the request, but I wondered if it could be done using promises only. So, starting with a simple fetch, let’s see if we can fit a timeout in there:

const load = fetch('/api').then(res => res.json());

I bet we can, and let me show you how! First things first, we need to pull a callback-based JS timeout into the promise world. Here’s how we do it:

new Promise((ok) => {
    setTimeout(ok, 5000);
});

Pretty basic stuff, and very useful. We use the promise constructor to set the timeout that resolves the promise created after 5 seconds.

The next bit we need is Promise.race, the little brother of the famous Promise.all that is useful for about one real world thing, so no one really cares about it. Quoting MDN, Promise.race() returns a promise that fulfills or rejects as soon as one of the promises in an iterable fulfills or rejects, with the value or reason from that promise. That’s exactly what we need!

  • If load promise resolves before timeout — we’re good to go.
  • If load promise rejects — fall back to plan B.
  • If the timeout fires first — fall back to plan B, just as if loading failed.

Putting it all together and tweaking our timeout-promise to make it reject (we’re bad, not good, if we hit it) gives us this wonderful snippet:

const load = Promise.race([
    fetch('/api').then(res => res.json()),
    new Promise((_, fail) => setTimeout(() => fail(new Error('Timeout')), 5000))
]).then(
    (res) => { /* process as you wish */ },
    (err) => { /* retry or display error */ });

What’s even better, this technique, unlike AbortSignal, works not only for fetch, but for any promise-based operation: just replace fetch call above with yourApiLayer.load(), DBQuery.execute(), serviceMesh.callRPC() or whatever async stuff you want to timeout, and you’re good to go.

More? All posts ever
Written in by your friend, Vladimir. Follow me on Twitter to get post updates. I have RSS, too. And you can buy me a coffee!
Older
Extravagantly fast rendering with React benders
Newer
How useRef turned out to be useMemo's father