The problem statement is: you want to execute a function that may fail, and you want to retry it N times. How could you solve this?
An initial approach can be to program it for a specific number of times. Let's run functionThatMayFail
and retry it 3 times
try {
functionThatMayFail();
} catch (e0) {
try {
// 1st retry
functionThatMayFail();
} catch (e1) {
try {
// 2nd retry
functionThatMayFail();
} catch (e2) {
try {
// 3rd retry
functionThatMayFail();
} catch (e3) {
// give up
handleException(e3);
}
}
}
}
The previous snippet has some problems:
- it involves duplication, and
- the number of retries can't be modified at runtime
By using recursion we could write a second approach that may address those disadvantages —however I would advocate to avoid it if possible, as this is not a recursive problem per se and the code will be harder to read and debug.
A third approach I'm more willing to follow is to make a utility function that takes care of both calling a function and its retries,
function callAndRetry(fn, maxAmountOfRetries) {
let error;
for (let retries = 0; retries < maxAmountOfRetries; retries++) {
try {
return fn();
} catch (err) {
error = err;
}
}
throw error;
}
In our example, this should be used as
try {
callAndRetry(functionThatMayFail, 3);
} catch (error) {
handleException(error);
}
The presented approach solves the original statement and doesn't have the disadvantages of the previous methods. Another good thing about this pattern is that can be repurposed for asynchronous functions as well. How so?
Let's say functionThatMayFail
now returns a Promise. Instead of hard-coding retries or going recursive, we can slightly modify callAndRetry
to wait for the function to fulfill, as
async function callAndRetry(fn, maxAmountOfRetries) {
let error;
for (let retries = 0; retries < maxAmountOfRetries; retries++) {
try {
return await fn();
} catch (err) {
error = err;
}
}
throw error;
}
Did you notice how we now use an async function and await
for the fn
function to fulfill? All the rest remains unchanged.
This new version can be used with the typical Promise chaining,
callAndRetry(functionThatMayFail, 3)
.catch(handleException);
But I believe there is even more value when it is used like a sync call:
// in the context of an async function, so we can `await`
(async () => {
try {
await callAndRetry(functionThatMayFail, 3);
} catch (error) {
handleException(error);
}
})();
And the reasons are:
- we use regular
try/catch
for error exception, and - this code can be used for both synchronous and asynchronous functions! Yes,
functionThatMayFail
may return a Promise or anything else and this would still works.
Another advantage of using a snippet to encapsulate the retry logic, instead of mixing it with the business logic, is that we can write it once and reused in your whole project as needed. You may even install one its versions available at NPM which include different retry strategies, if you need them.
I read your feedback in the comments below. Happy retrying!
🙏🙏🙏
Since you've made it this far, sharing this article on your favorite social media network would be highly appreciated 😻! For feedback, please ping me on Mastodon.
In case you'd like to keep reading: