Skip to content

Instantly share code, notes, and snippets.

@domenic
Last active September 16, 2023 02:43
Show Gist options
  • Save domenic/2936696 to your computer and use it in GitHub Desktop.
Save domenic/2936696 to your computer and use it in GitHub Desktop.
Generalized promise retryer
"use strict";
// `f` is assumed to sporadically fail with `TemporaryNetworkError` instances.
// If one of those happens, we want to retry until it doesn't.
// If `f` fails with something else, then we should re-throw: we don't know how to handle that, and it's a
// sign something went wrong. Since `f` is a good promise-returning function, it only ever fulfills or rejects;
// it has no synchronous behavior (e.g. throwing).
function dontGiveUp(f) {
return f().then(
undefined, // pass through success
function (err) {
if (err instanceof TemporaryNetworkError) {
return dontGiveUp(f); // recurse
}
throw err; // rethrow
}
});
}
// Analogous synchronous code:
function dontGiveUpSync(fSync) {
try {
return fSync();
} catch (err) {
if (err instanceof TemporaryNetworkError) {
return dontGiveUpSync(fSync);
}
throw err;
}
}
// Note how we created this powerful abstraction without ever using ANY aspect of the promise implementation
// besides the Promises/A "thenable"-ness. In particular, we had NO need of a deferred library, and we can
// interoperate with ANY promise library, returning to the caller the same type of promise we received (i.e.
// we don't assimilate When.js promises into Q promises in order to work with them).
// An example of a library that makes extensive use of this property is
// https://github.com/domenic/chai-as-promised/
// Unfortunately such composable patterns are impossible with jQuery promises, since there is no way to trap
// and rethrow errors. jQuery only allows you to catch explicit rejections, but does not allow access to
// thrown exceptions, and furthermore you cannot transition into a rejected state by (re)throwing or out
// of one by choosing not to rethrow: in a jQuery promise's fulfilled and rejected callbacks, you need to
// manually use jQuery's deferred library to transition state.
@hh10k
Copy link

hh10k commented Jan 17, 2013

These examples will overflow the stack if left to run. You would normally write the sync version as an infinite loop.

function dontGiveUpSync(fSync) {
    for (;;) {
        try {
            return fSync();
        } catch (err) {
            if (!(err instanceof TemporaryNetworkError)) {
                throw err;
            }
        }
    }
}

If f() is actually synchronous too, does Promises/A say anything about preventing stack overflows? Here's my guaranteed stack-safe version for promises:

function dontGiveUp(f) {
    var result;
    var again = true;
    while (again) {
        again = false;
        result = null;
        result = f().then(
            null,
            function (err) {
                if (!(err instanceof TemporaryNetworkError)) throw err;

                // If we see the result now, it was done asynchronously
                if (result) return dontGiveUp(f);

                // Completed before returned, make it try again
                again = true;
            }
        );
    }
    return result;
}

@domenic
Copy link
Author

domenic commented May 14, 2013

@hh10k Promises/A+ implementations are necessarily asynchronous, and thus avoid stack overflows without awkward contortions like that above.

@Dao007forever
Copy link

@hh10k The 2nd code seems wrong, again will be false all the time since in this tick, the error handler is not called yet?

@tuscland
Copy link

The second example will not work, it is written with synchronous calls in mind.

@xmlking
Copy link

xmlking commented Jul 16, 2014

Trying to achieve delay between retries based on @domenic code. But not working :(

export default class Resiliency {

    /**
     * Retry failed promise functions N times.
     * TODO: exclude retrying certain exceptions  
     * TODO: add delay between retries
     */
    static retry(operation, maxTimes = 5) {
        if (!this.isFunction(operation)) {
            throw new TypeError('first argument mush be a function that returns a promise');
        }
        console.info(`Attempt #${maxTimes}`);

        return operation().catch((error) => {

            if (maxTimes === 0) {
                throw new Error('Giving up! maximum retry attempts reached. Original Exception: '+ error);
            }
            console.error('Error:  ' + error);
            return this.retry(operation, maxTimes - 1);


//            setTimeout( () => {
//                return this.retry(operation, maxTimes - 1);
//            }, 10000);
        });
    }

    //static isFunction(value){return typeof value === 'function';}
    static isFunction(value){return value instanceof Function;}
    static isPromise(value){return value && this.isFunction(value.then);}
}

Test Case:

import Resiliency from '../../app/scripts/common/utils/Resiliency';

describe('Resiliency', function () {
    'use strict';
    let promiseFunction;

    beforeEach(function () {
        promiseFunction = () => {
            return new Promise((resolve, reject) => {
                let testVal = Math.floor((Math.random() * 10) + 1);
                if(testVal === 7) {resolve(`testVal=${testVal} is equal to 7`);}
                else {reject(`testVal=${testVal} is not equal to 7`);}
            });
        };
    });

    it('should work', function () {
        Resiliency.retry(promiseFunction , 3)
            .then((success) => {console.log('Success: ' +success);})
            .catch( (error) => {console.error(error.message);});
    });

});

@pablolmiranda
Copy link

@xmlking the problems is after you call operation() function the promise will be resolved or rejected.
Your code will work if the promise returned by operation could be moved to pending state again inside the catch block. So the operation can be executed again.

@andig
Copy link

andig commented Oct 14, 2014

Still a good question how to introduce delays in the retry function. I didn't find a solution :(

@eladchen
Copy link

@andig Isn't this a simple operation?

disclosure: this was not tested 😀

/** 
 * @param {wait} - the time in milliseconds to delay the second attempt,
 * will default to 0 if not given
 *
 * @param {firstCall} - a boolean to determine if this is the first cycle, 
 * will default to true if not provided 
 */
function dontGiveUp(f, wait, firstCall) {
    var firstCall = typeof firstCall == "undefined" ? true : false
    var wait      = typeof wait == "number" ? wait : 0

    /** 
     * @NOTE: the delay function wrapper will be called anyway.
     * delaying is only the desired starting from the second attempt
     */
    var errorHandler = function (err) {
      if (err instanceof TemporaryNetworkError) {
        return dontGiveUp(f, wait, false); // recurse
      }
      throw err; // rethrow
    }

    return f().then(undefined, function(err) {
        setTimeout(errorHandler, firstCall ? 0 : wait)
    })
}

@icodeforlove
Copy link

I find myself needing this type of solution a lot, i wonder if theres anything that supports

  • custom delays based on attempts
  • maxRetries
  • promise result validation (if it fails then keep trying)

@icodeforlove
Copy link

@aartajew
Copy link

Working version with delay and try count limit:

function retry(func, count = 5, delay = 1000) {
  return func().then(undefined, (e) => {
    if (count - 1) {
      return new Promise((resolve) => setTimeout(resolve, delay)).then(() =>
        retry(func, count - 1, delay)
      );
    }
    throw e;
  });
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment