Published on

Retry, Timeout and Cancel with fetch()

Authors

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
    if (retryCount < maxRetries) {
      return fetchWithRetries(url, options, retryCount + 1)
    }
    // max retries exceeded
    throw error
  }
}

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)
}

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]
}

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()