Promise Retry Design Patterns

Promise Retry Design Patterns

Something a bit different ...

Async retries can be achieved by building a .catch() chain, as opposed to the more usual .then() chain.

This approach is :

  • only possible with a specified maximum number of attempts. (The chain must be of finite length),
  • only advisable with a low maximum. (Promise chains consume memory roughly proportional to their length).

Otherwise, use a recursive solution.

First, a utility function to be used as a .catch() callback.

var t = 500;

function rejectDelay(reason) {
return new Promise(function(resolve, reject) {
setTimeout(reject.bind(null, reason), t);
});
}

Now you can build .catch chains very concisely :

1. Retry until the promise resolves, with delay

var max = 5;
var p = Promise.reject();

for(var i=0; i<max; i++) {
p = p.catch(attempt).catch(rejectDelay);
}
p = p.then(processResult).catch(errorHandler);

DEMO: https://jsfiddle.net/duL0qjqe/

2. Retry until result meets some condition, without delay

var max = 5;
var p = Promise.reject();

for(var i=0; i<max; i++) {
p = p.catch(attempt).then(test);
}
p = p.then(processResult).catch(errorHandler);

DEMO: https://jsfiddle.net/duL0qjqe/1/

3. Retry until result meets some condition, with delay

Having got your mind round (1) and (2), a combined test+delay is equally trivial.

var max = 5;
var p = Promise.reject();

for(var i=0; i<max; i++) {
p = p.catch(attempt).then(test).catch(rejectDelay);
// Don't be tempted to simplify this to `p.catch(attempt).then(test, rejectDelay)`. Test failures would not be caught.
}
p = p.then(processResult).catch(errorHandler);

test() can be synchronous or asynchronous.

It would also be trivial to add further tests. Simply sandwich a chain of thens between the two catches.

p = p.catch(attempt).then(test1).then(test2).then(test3).catch(rejectDelay);

DEMO: https://jsfiddle.net/duL0qjqe/3/


All versions are designed for attempt to be a promise-returning async function. It could also conceivably return a value, in which case the chain would follow its success path to the next/terminal .then().

How to retry a Promise resolution N times, with a delay between the attempts?

I did not want to use a recursive function: this way, even if there are 50 attempts the call stack isn't 50 lines longer.

That's not a good excuse. The call stack doesn't overflow from asynchronous calls, and when a recursive solution is more intuitive than an iterative one you should probably go for it.

What I ended up doing is using a for loop. Is this a "good" way of doing it, and if not, how I could improve it?

The for loop is fine. It's a bit weird that it starts at 1 though, 0-based loops are much more idiomatic.

What is not fine however is your weird error handling. That boolean error flag should have no place in your code. Using .catch() is fine, but try/catch would work just as well and should be preferred.

export async function tryNTimes<T>({ toTry, times = 5, interval = 1}) {
if (times < 1) throw new Error(`Bad argument: 'times' must be greater than 0, but ${times} was received.`);
let attemptCount = 0
while (true) {
try {
const result = await toTry();
return result;
} catch(error) {
if (++attemptCount >= times) throw error;
}
await delay(interval)
}
}

Promise.allSettled() - Retry strategy for multiple async calls

Promises do not keep any record of where they came from: Once you have a Promise, there's nothing that would allow you to retry its origin multiple times. As such, common Promise retry patterns accept the promise-returning function ("promise factory"), not just the Promise itself. Wrapping each individual promise in a retry function like this before calling all or allSettled is the most practical solution, since promises that fail quickly can try again immediately without waiting for the entire list like allSettled does.

const multiAsync = [
retry(() => this.somethingA(), 3),
retry(() => this.somethingB(), 3),
retry(() => this.somethingC(), 3),
];
const [a, b, c] = await Promise.allSettled(multiAsync);

// or

const [a, b, c] =
await Promise.allSettled([/* functions */].map(x => retry(x, 3));

However, if you'd like to see how to use Promise.allSettled to do this directly, I have one here. My solution here doesn't respect timeouts, which you can add globally or individually with a Promise.race implementation.

/**
* Resolves a series of promise factories, retrying if needed.
*
* @param {number} maxTryCount How many retries to perform.
* @param {Array<() => Promise<any>>} promiseFactories Functions
* that return promises. These must be functions to enable retrying.
* @return Corresponding Promise.allSettled values.
*/
async function allSettledWithRetry(maxTryCount, promiseFactories) {
let results;
for (let retry = 0; retry < maxTryCount; retry++) {
let promiseArray;
if (results) {
// This is a retry; fold in results and new promises.
promiseArray = results.map(
(x, index) => x.status === "fulfilled"
? x.value
: promiseFactories[index]())
} else {
// This is the first run; use promiseFactories only.
promiseArray = promiseFactories.map(x => x());
}
results = await Promise.allSettled(promiseArray);
// Avoid unnecessary loops, though they'd be inexpensive.
if (results.every(x => x.status === "fulfilled")) {
return results;
}
}
return results;
}

/* test harness below */

function promiseFactory(label) {
const succeeds = Math.random() > 0.5;
console.log(`${label}: ${succeeds ? 'succeeds' : 'fails'}`);
return succeeds
? Promise.resolve(label)
: Promise.reject(new Error(`Error: ${label}`));
}

allSettledWithRetry(5, [
() => promiseFactory("a"),
() => promiseFactory("b"),
() => promiseFactory("c"),
() => promiseFactory("d"),
() => promiseFactory("e"),
]).then(console.log);

Retry promise with delay until resolved in NodeJS

I'd do it by making the function that does a single connection attempt (basically, renaming your connect to tryConnect or similar, perhaps even as a private method if you're using a new enough version of Node.js), and then having a function that calls it with the repeat and delay, something like this (see comments):

Utility function:

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

The new connect:

async connect() {
for (let attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) {
if (attempt > 0) {
// Last attempt failed, wait a moment
await delay(RETRY_DELAY_IN_MS);
}
try {
await tryConnect();
return; // It worked
} catch {
}
}
// Out of retries
throw new Error("Couldn't create connection");
}

(If you're using a slightly older Node.js, you may need to add (e) after the catch above. Leaving it off when you don't need it is a relatively new feature.)


Re your current implementation of what I'm calling tryConnect, here are a few notes as comments for how I'd change it:

tryConnect() {
return new Promise((resolve, reject) => {
// Keep these local for now
const socket = new net.Socket();
const client = new Modbus.client.TCP(socket, this.unitID);

// Add handlers before calling `connect
socket.on('connect', () => {

logger.info('*****CONNECTION MADE*****');

// NOW save these to the instance and resolve the promise
this.socket = socket;
this.client = client;
resolve();
});

socket.on('error', (error) => {
logger.error('failed to connect');
// It's not connected, so no `disconnect` call here
reject();
});

socket.connect(options);
});
}

Retry on Javascript.Promise.reject a limited number of times or until success

Using a couple of helper functions I've used a lot, this becomes very easy

The "helpers"

Promise.wait = (time) => new Promise(resolve => setTimeout(resolve, time || 0));
Promise.retry = (cont, fn, delay) => fn().catch(err => cont > 0 ? Promise.wait(delay).then(() => Promise.retry(cont - 1, fn, delay)) : Promise.reject('failed'));

The code:

function myMainFuntion() {
var delay = 100;
var tries = 3;
Promise.retry(tries, tryAsync, delay);
}

ES5 versions of the helpers

Promise.wait = function (time) {
return new Promise(function (resolve) {
return setTimeout(resolve, time || 0);
});
};
Promise.retry = function (cont, fn, delay) {
return fn().catch(function (err) {
return cont > 0 ? Promise.wait(delay).then(function () {
return Promise.retry(cont - 1, fn, delay);
}) : Promise.reject('failed');
});
};

How to Retry a Promise in ES6

Your approach would work. When a promise is passed to the resolve callback of the Promise constructor, the resolution value of that promise is used as the resolution value of the "outer" promise.

It could be cleaner to wrap ajaxcall in a promise-returning function so that you're not mixing and matching promise and callback paradigms.

function fetch(url) {
return new Promise(function(resolve, reject) {
ajaxcall({
url: url,
success: resolve,
failure: reject
});
});
}

function getdata(url) {
return fetch(url)
.catch(function(err) {
if(retriable(err)) {
return getdata(url); // or fetch(url) to retry only once
} else {
throw err;
}
});
}

How to retry an async function with a delay in javascript?

That promise.reject()/promise.resolve() approach is not going to work, you cannot resolve a promise from the outside. And you shouldn't need to - just return/throw from your async function! The only place where you need to construct a new Promise is in a little helper function

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

Then you can write your function in a recursive manner:

async function tryFetchBooking(
id,
max_retries = 3,
current_try = 0,
) {
let booking = await strapi.query("api::booking.booking").findOne({
where: {
id: id,
},
});

if (!booking) {
if (current_try < max_retries) {
console.log("No booking. Retrying");
await delay(500);
// ^^^^^^^^^^^^^^^^
booking = await tryFetchBooking(id, max_retries, current_try + 1);
// ^^^^^^^^^^^^^^^^^^^^^
console.log("Found booking with retry");
} else {
console.log("No booking. Giving up.");
throw new Error("no booking found in time");
// or if you prefer the other error message:
throw new Error(`Failed retrying 3 times`);
}
}
return booking;
}

or even in an iterative manner:

async function tryFetchBooking(id, maxRetries = 3) {
let currentTry = 0;
while (true) {
const booking = await strapi.query("api::booking.booking").findOne({
where: {
id: id,
},
});

if (booking) {
return booking;
}
if (currentTry < maxRetries) {
await delay(500);
currentTry++;
} else {
console.log("No booking. Giving up.");
throw new Error("no booking found in time");
}
}
}

Native JS Promise Retry Wrapper

Here's a wrapper around your makeRequest() function that lets you pass in the max retry count and the delay between retries. It returns a promise that resolves if successful or rejects if it still has an error after maxRetries.

function makeRequestRetry(requestMethod, navToURL, maxRetry = 3, retryDelay = 3000) {
let retryCnt = 0;

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

function run() {
return makeRequest(requestMethod, navToURL).catch(function (err) {
++retryCnt;
if (retryCnt > maxRetry) {
console.error('Max retries exceeded. There was an error!', err.statusText);
throw err;
}
console.error('Retry #' + retryCnt + ' after error', err.statusText);
// call ourselves again after a short delay to do the retry
// add to the promise chain so still linked to the originally returned promise
return delay(retryDelay).then(run);
});

}
return run();
}

And, one would call it like this:

makeRquestRetry('GET', someURL, 5, 5000).then(result => {
console.log(result);
}).catch(err => {
console.log(err);
});

Or, go with the default values for retry parameters:

makeRquestRetry('GET', someURL).then(result => {
console.log(result);
}).catch(err => {
console.log(err);
});


Related Topics



Leave a reply



Submit