22

I'm using browser's native fetch API for network requests. Also I am using the whatwg-fetch polyfill for unsupported browsers.

However I need to retry in case the request fails. Now there is this npm package whatwg-fetch-retry I found, but they haven't explained how to use it in their docs. Can somebody help me with this or suggest me an alternative?

vsync
  • 118,978
  • 58
  • 307
  • 400
dsaket
  • 1,827
  • 2
  • 19
  • 30
  • Does this answer your question? [How to try JS fetch() method in a loop](https://stackoverflow.com/questions/60289882/how-to-try-js-fetch-method-in-a-loop) – ggorlen Jan 31 '21 at 19:29

5 Answers5

28

From the fetch docs :

fetch('/users')
    .then(checkStatus)
    .then(parseJSON)
    .then(function(data) {
          console.log('succeeded', data)
    }).catch(function(error) {
          console.log('request failed', error)
    })

See that catch? Will trigger when fetch fails, you can fetch again there. Have a look at the Promise API.

Implementation example:

function wait(delay){
    return new Promise((resolve) => setTimeout(resolve, delay));
}

function fetchRetry(url, delay, tries, fetchOptions = {}) {
    function onError(err){
        triesLeft = tries - 1;
        if(!triesLeft){
            throw err;
        }
        return wait(delay).then(() => fetchRetry(url, delay, triesLeft, fetchOptions));
    }
    return fetch(url,fetchOptions).catch(onError);
}

Edit 1: as suggested by golopot, p-retry is a nice option.

Edit 2: simplified example code.

Isidrok
  • 2,015
  • 10
  • 15
  • This is exactly what I'm currently doing. But I'm looking for something like an additional optional { retry: 3, //max number of retries retryTimeout: 3000 //timeout between successive requests } – dsaket Sep 12 '17 at 13:18
  • I just updated the example with what I think should be a working version, you may need some tweak tough. Tell me if u want some comments in the code to make it clearer. – Isidrok Sep 12 '17 at 19:10
  • This is [needlessly complex](https://stackoverflow.com/questions/23803743/what-is-the-explicit-promise-construction-antipattern-and-how-do-i-avoid-it) – Benjamin Gruenbaum May 14 '20 at 10:35
  • Thansk for pointing that out @BenjaminGruenbaum the code was pretty messy – Isidrok Jun 14 '20 at 21:27
11

I recommend using some library for promise retry, for example p-retry.

Example:

const pRetry = require('p-retry')
const fetch = require('node-fetch')

async function fetchPage () {
  const response = await fetch('https://stackoverflow.com')

  // Abort retrying if the resource doesn't exist
  if (response.status === 404) {
    throw new pRetry.AbortError(response.statusText)
  }

  return response.blob()
}

;(async () => {
  console.log(await pRetry(fetchPage, {retries: 5}))
})()
golopot
  • 10,726
  • 6
  • 37
  • 51
7

I don't like recursion unless is really necessary. And managing an exploding number of dependencies is also an issue. Here is another alternative in typescript. Which is easy to translate to javascript.

interface retryPromiseOptions<T>  {
    retryCatchIf?:(response:T) => boolean, 
    retryIf?:(response:T) => boolean, 
    retries?:number
}

function retryPromise<T>(promise:() => Promise<T>, options:retryPromiseOptions<T>) {
    const { retryIf = (_:T) => false, retryCatchIf= (_:T) => true, retries = 1} = options
    let _promise = promise();

    for (var i = 1; i < retries; i++)
        _promise = _promise.catch((value) => retryCatchIf(value) ? promise() : Promise.reject(value))
           .then((value) => retryIf(value) ? promise() : Promise.reject(value));
    
    return _promise;
}

And use it this way...

retryPromise(() => fetch(url),{
    retryIf: (response:Response) => true, // you could check before trying again
    retries: 5
}).then( ... my favorite things ... )

I wrote this for the fetch API on the browser. Which does not issue a reject on a 500. And did I did not implement a wait. But, more importantly, the code shows how to use composition with promises to avoid recursion.

Javascript version:

function retryPromise(promise, options) {
    const { retryIf, retryCatchIf, retries } = { retryIf: () => false, retryCatchIf: () => true, retries: 1, ...options};
    let _promise = promise();

    for (var i = 1; i < retries; i++)
        _promise = _promise.catch((value) => retryCatchIf(value) ? promise() : Promise.reject(value))
           .then((value) => retryIf(value) ? promise() : Promise.reject(value));
    
    return _promise;
}

Javascript usage:

retryPromise(() => fetch(url),{
    retryIf: (response) => true, // you could check before trying again
    retries: 5
}).then( ... my favorite things ... )

EDITS: Added js version, added retryCatchIf, fixed the loop start.

Arturo Hernandez
  • 2,749
  • 3
  • 28
  • 36
7

One can easily wrap fetch(...) in a loop and catch potential errors (fetch only rejects the returning promise on network errors and the alike):

const RETRY_COUNT = 5;

async function fetchRetry(...args) {
  let count = RETRY_COUNT;
  while(count > 0) {
    try {
      return await fetch(...args);
    } catch(error) {
      // logging ?
    }

    // logging / waiting?

    count -= 1;
  }

  throw new Error(`Too many retries`);
}  
Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
1

Using both Jonas Wilms and Isidrock answers and adding typescript types:

const MAX_NB_RETRY = 5;
const RETRY_DELAY_MS = 200;

export default async function fetchRetry(input: RequestInfo | URL, init?: RequestInit | undefined) {
    let retryLeft = MAX_NB_RETRY;
    while (retryLeft > 0){
        try {
            return await fetch(input, init);
        }
        catch (err) { 
            await sleep(RETRY_DELAY_MS)
        }
        finally {
            retryLeft -= 1;
        }
    }
    throw new Error(`Too many retries`);
}

function sleep(delay: number){
    return new Promise((resolve) => setTimeout(resolve, delay));
}
cglacet
  • 8,873
  • 4
  • 45
  • 60