Tips from open-source: Set a maximum time limit on fetch using Promise.race()
Tags: javascript, node, nextjs, react
Published: 2024-05-07 21:51:03

This article demonstrates how Promise.race can be used to set a maximum time limit on a fetch request. Originally, I found [Promise.race()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race) in the [next.js source code](https://github.com/vercel/next.js/blob/canary/packages/next/src/lib/worker.ts#L123).

> [_Learn the best practices used in open source_](https://tthroo.com/)

Promise.race()
--------------

Promise.race requires an iterable of promises and returns a promise that settles first in the provided iterable promises.

```
// source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global\_Objects/Promise/race
const promise1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, 'one');
});

const promise2 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'two');
});

Promise.race(\[promise1, promise2\]).then((value) => {
  console.log(value);
  // Both resolve, but promise2 is faster
});
// Expected output: "two"
```

I noticed Next.js uses Promise.race in a [worker related file](https://github.com/vercel/next.js/blob/canary/packages/next/src/lib/worker.ts) and is quite advanced.

```
// source: https://github.com/vercel/next.js/blob/canary/packages/next/src/lib/worker.ts#L121C15-L129C16
for (;;) {
  onActivity()
  const result = await Promise.race(\[
    (this.\_worker as any)\[method\](...args),
    restartPromise,
  \])
  if (result !== RESTARTED) return result
  if (onRestart) onRestart(method, args, ++attempts)
}
```

I wanted to pick an example that is easy to understand. So, I [searched in the wild](https://github.com/search?q=promise.race&type=code).

![search results on github](https://media.licdn.com/dms/image/D4E12AQEtLhHjrXdapw/article-inline_image-shrink_1500_2232/0/1715117174457?e=1720656000&v=beta&t=zK5YUEzF7m25-ytR4JrbEOk2VyeI5hNdhz8pdyC95JY)

That is when I found [ChatGPT-Next-Web](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web) uses Promise.race(\[fetch(CN\_URL), timeoutPromise(5000)\]).

### timeoutPromise

```
const timeoutPromise = (timeout) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error("Request timeout"));
    }, timeout);
  });
};
```

### Promise.race(\[fetch(EN\_URL), timeoutPromise(5000)\])

Promise.race(\[fetch(EN\_URL), timeoutPromise(5000)\]) — What this means is that fetch should finish executing in 5 seconds otherwise, it is timed out with a promise rejected.

Upon closer inspection, I found EN\_URL and CN\_URL translate to [http://raw.fgit.ml/f/awesome-chatgpt-prompts/main/prompts.csv](http://raw.fgit.ml/f/awesome-chatgpt-prompts/main/prompts.csv) and [http://raw.fgit.ml/PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh.json](http://raw.fgit.ml/PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh.json) respectively.

```
// source: https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/blob/3513c6801e0fd771ccb5784dcaefcac6d50245e4/scripts/fetch-prompts.mjs#L26
async function main() {
  Promise.all(\[fetchCN(), fetchEN()\])
    .then((\[cn, en\]) => {
      fs.writeFile(FILE, JSON.stringify({ cn, en }));
    })
    .catch((e) => {
      console.error("\[Fetch\] failed to fetch prompts");
      fs.writeFile(FILE, JSON.stringify({ cn: \[\], en: \[\] }));
    })
    .finally(() => {
      console.log("\[Fetch\] saved to " + FILE);
    });
}

main();
```

If the fetch is not resolved in 5 seconds, timeoutPromise rejects and the error is caught in catch block that writes to file with stringified {cn: \[\], en: \[\] } as shown above.

Other ways to abort a fetch request
-----------------------------------

I googled and found the following stackoverflow answer to be insightful

https://stackoverflow.com/questions/46946380/fetch-api-request-timeout

Using a promise race solution will leave the request hanging and still consume bandwidth in the background and lower the max allowed concurrent request being made while it’s still in process.

Instead use the [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) to actually abort the request, Here is an example

```
const controller = new AbortController()

// 5 second timeout:

const timeoutId = setTimeout(() => controller.abort(), 5000)

fetch(url, { signal: controller.signal }).then(response => {
  // completed request before timeout fired

  // If you only wanted to timeout the request, not the response, add:
  // clearTimeout(timeoutId)
})
```

— Picked from the above stackoverflow answer.

Conclusion
----------

Usage of Promise.race depends on your usecase. I found that Next.js source code uses Promise.race in an infinite for loop with a breaking condition in a worker related file but I wanted to pick an example that is easy to understand. So, I [searched in the wild](https://github.com/search?q=promise.race&type=code).

And found a simple, easy to understand example, Promise.race can be used to set a maximum time limit on fetch request but this request fetches the prompts as csv and json in ChatGPT-Next-Web.

However, stackoverflow answer suggests that “Using a promise race solution will leave the request hanging and still consume bandwidth in the background and lower the max allowed concurrent request being made while it’s still in process.”

This makes me conclude that, use Promise.race only if you are fine with the fetch request hanging, otherwise use AbortController to abort the fetch request altogether.