DEV Community

Chris Armstrong
Chris Armstrong

Posted on • Edited on • Originally published at chrisarmstrong.dev

Retry, Timeout and Cancel with fetch()

(This was originally posted at my development blog on chrisarmstrong.dev. Check it out for more AWS and DynamoDB related articles!)

Although none of these are built-in, it's easy to add retries, timeouts and cancellations to your fetch() calls without needing to use a full-featured third-party library.

Retries

By wrapping your fetch handler in a recursive function that returns a promise, you can easily get retry behaviour:


const fetchWithRetries = async (
url, options, retryCount = 0) => {
  // split out the maxRetries option from the remaining options
  // (with a default of 3 retries)
  const { maxRetries = 3, ...remainingOptions } = options;
  try {
    return await fetch(url, remainingOptions);
  } catch (error) {
    // if the retryCount has not been exceeded, call again with retryCount + 1
    if (retryCount < maxRetries) {
      return fetchWithRetries(url, options, retryCount + 1)
    }
    // max retries exceeded, just throw error
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

You could even customise it further with additional flags to control when to retry, or to throw a MaxRetriesError when the maxRetries count is hit.

Timeout

This one is fairly easy - we use setTimeout to reject the promise when there is a timeout error.


// Create a promise that rejects after `timeout` milliseconds
const throwOnTimeout = (timeout) => 
  new Promise((_, reject) => 
    setTimeout(() => 
     reject(new Error("Timeout")),
     timeout
    ),
  );

const fetchWithTimeout = (url, options = {}) => {
  const { timeout, ...remainingOptions } = options;
  // if the timeout option is specified, race the fetch call
  if (timeout) {
    return Promise.race(fetch(url, remainingOptions), throwOnTimeout(timeout));
  }
  return fetch(url, remainingOptions);
}
Enter fullscreen mode Exit fullscreen mode

We use Promise.race to run both the fetch() call and the setTimeout() call at the same time - whichever resolves or rejects first will resolve or reject the Promise (respectively), with the other result being ignored.

Cancel

This one is documented on MDN, but for completeness here it is below.

const fetchWithCancel = (url, options = {}) => {
  const controller = new AbortController();
  const call = fetch(url, { ...options, signal: controller.signal });
  const cancel = () => controller.abort();
  return [call, cancel ];
};
Enter fullscreen mode Exit fullscreen mode

In the above, we return a tuple with the returned promise and a cancel function that allows the user to cancel the request if needed.

An example showing its usage is below:

// We don't await this call, just capture the promise
const [promise, cancel] = fetchWithCancel('https://cataas.com/cat?json=true');

// await the promise to get the response
const response = await promise;

// ...
// cancel the request (e.g. if we have rendered something else)
cancel();

Enter fullscreen mode Exit fullscreen mode

Top comments (0)