Timing Out Fetch Requests

Last reviewed on January 18, 2022

Recently I learned that the fetch() function in JavaScript in the browser doesn't have a timeout for requests. A fetch() request will hang until the server responds. This can be exhibited by creating an endpoint in Express that doesn't respond and making a fetch() request to it:

server.js
const express = require("express");
const app = express();

app.get("/posts", async (request, response) => {
  // no response sent
});

app.use(express.static("public"));

app.listen(8080);
public/index.html
<script>
  fetch("/posts");
</script>
Terminal
npm install express
node server.js

Now visit http://localhost:8080/index.html. If you look at the Network panel in Chrome, you'll see that the request status will show as pending.

In Understanding JavaScript Promises by Nicholas Zakas, he goes through an example where we can use Promise.race() to add a timeout to any request. Note, there is a free version of the book containing the first three chapters. Despite working with promises regularly, I learned some new things even from the free version so I bought the full version. Lots of great content even for experienced JavaScript developers. Anyways, here is the example in Chapter 3:

function timeout(milliseconds) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error("Request timed out."));
    }, milliseconds);
  });
}

function fetchWithTimeout(...args) {
  return Promise.race([fetch(...args), timeout(5000)]);
}

fetchWithTimeout() can be a replacement for fetch() as it has the same API. The only difference is that it has a timeout of 5 seconds with the help of Promise.race(). Promise.race() returns a promise that fulfills or rejects as soon as one of the promises fulfills or rejects, with the value or error from that promise.

At the end of the section, Nicholas mentioned that the request won't be cancelled and it'll be waiting for a response behind the scenes even though the response will be ignored. To cancel a fetch request that exceeds the timeout, we can use AbortController. I have modified the code above to make use of AbortController so that any long running requests get cancelled:

public/main.js
function fetchWithTimeout(resource, init = {}) {
  const controller = new AbortController();

  return Promise.race([
    fetch(resource, {
      ...init,
      signal: controller.signal,
    }),
    timeout(5000),
  ]).catch(error => {
    if (error.message === 'Request timed out.') {
      controller.abort();
    }

    return Promise.reject(error);
  });
}
public/index.html
<script src="/main.js"></script>
<script>
  fetchWithTimeout("/posts");
</script>

By making the request above using fetchWithTimeout('/posts') instead of fetch('/posts'), we will see that our request will now show as cancelled in the Network tab in Chrome after 5 seconds.