sookie
sookie

Reputation: 2517

ES6 - Conditionally signalling a retry within a Promise until max retries has been reached

I have seen a number of questions around retrying Promises, however what I'm looking to do is slightly different in that I'd like to manage the retrying/rejecting of promises conditionally until the max retries have been reached.

To give a simple example, imagine we wrap a promise around an XMLHttpRequest. When the request loads with a status of...

Notice that there is scope here for asynchronous behavior to be executed before retries.

The solution I have been looking into involves two Promises.

Bringing this back to the example I mentioned...

I think I'm going in the right direction with this, but can't seem to get a concrete solution in place. I'm looking to create a generic wrapper for this kind of 'conditionally retrying promise.'


Edit:

Here is a solution in progress:

async function tryAtMost(maxAttempts, asyncCall, handleError)
{
    for (let i = 0; i < maxAttempts; i++)
    {
        try 
        { 
            return await asyncCall(); 
        }
        catch (error)
        {
            const nextAction = await handleError(error); // await some async request (if available) before proceeding
            const actionError = new Error(nextAction.error);

            switch (nextAction.type)
            {
                case ACTIONS.ABORT:
                    throw actionError;
                case ACTIONS.RETRY:
                    if (i === maxAttempts - 1) { throw actionError; }
                    else { continue; }
            }
        }
    }
}

Upvotes: 4

Views: 837

Answers (3)

nicholaswmin
nicholaswmin

Reputation: 22949

Based off your comment:

I'm looking to create a generic wrapper for this kind of "conditionally" retrying promise.

Here's a more generalised wrapper for this:

  • It allows you to specify the number of max attempts.
  • You pass it your own Promise.
  • You specify, at the construction site, what should happen if the promise rejects and the max attempts have not yet been reached.

// Class Retryable

class Retryable {
  constructor({ promise, maxAttempts = 1, attemptRetry }) {
    this.promise = promise
    this.maxAttempts = maxAttempts
    this.attemptRetry = attemptRetry

    this.attempts = 0
  }

  generateTry() {
    console.info('generating request')

    return this.promise().catch(err => {
      if (++this.attempts === this.maxAttempts) throw err

      return this.attemptRetry(err, () => this.generateTry() , () => {
        throw err
      })
    })
  }
}

// Usage

const retryable = new Retryable({
  maxAttempts: 4,
  promise: () => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        reject({ status: 500 })
        // If you `resolve` here instead you will trigger `.then()`
      }, 200)
    })
  },

  attemptRetry: function(err, yes, no) {
    switch (err.status) {
      case 500:
        return yes()
        break;
      default:
        return no()
    }
  }
})

retryable.generateTry().then(result => {
  console.log(result)
}).catch(err => {
  console.error(err)
})

Upvotes: 1

nicholaswmin
nicholaswmin

Reputation: 22949

I would simply create a Class that returns an async function (which returns a Promise).

  • The Class instance keeps track of the attempts.
  • The async function attempts to fetch something x number of times, equal to the number of maxAttempts.
  • If the request responds properly without any errors just return the result.
  • Otherwise keep trying until you exhaust the number of maxAttempts.

An example for Node.js using request-promise-native:

const rp = require('request-promise-native')

class RetryableFetch {
  constructor({ url, maxAttempts = 3 }) {
    this.url = url
    this.maxAttempts = maxAttempts    
    this.attempts = 0

    return this.generateRequest()
  }

  async generateRequest() {
    for (let i = 0; i < this.maxAttempts; i++) {
      try {
        return await rp(this.url)
      } catch(err) {
        switch (err.statusCode) {
          // Add more cases here as you see fit.
          case 399:
            throw err
            break;
          default:
            if (++this.attempts === this.maxAttempts) throw err
        }
      }
    }
  }
}

Usage:

new RetryableFetch({
  url: 'https://www.google.com'
})
.then(result => {
  console.log(result)
})
.catch(err => {
  console.error(err)
})

You can of course substitute rp with Fetch if you want this to work in the browser since both use a Promise-based API.

Upvotes: 1

loganfsmyth
loganfsmyth

Reputation: 161457

There are a few ways to do this, as the other post shows. Personally I find the usage of class unnecessary. I'd approach it using something like

async function fetchWithRetries(theURL, remainingRetries = 5) {
  const response = await fetch(theURL);

  switch (response.status) {
    case 200:
      return await response.json(); // or whatever you need
    case 299:
      if (remainingRetries === 0) {
        throw new Error();
      }
      return await fetchWithRetries(theURL, remainingRetries - 1);
    case 399:
      throw new Error();
    case 499:
      if (remainingRetries === 0) {
        throw new Error();
      }

      const otherData = await fetchOtherData();

      return await fetchWithRetries(theURL, remainingRetries - 1);

    default:
      // TODO: You didn't specify other codes?
  }
}

Upvotes: 2

Related Questions