Implementing a retrier
Sometimes, you need to be able to retry an operation several time, until it succeeds (or give up after several tries). There are a lot of ways to implement this.
As a base, let's use a function called pause
allowing you to wait for some time between your tries:
function pause(delay = 100) {
return new Promise(resolve => setTimeout(resolve, delay))
}
A good old for loop
Now, a simple approach to implement a retrier would be to use a classic for loop:
async function retrier(operation, { attempts = Infinity, delay = 100 })
for (let i = 0 ; i < maxAttempts ; i++) {
const result = await operation()
if (result) return result
await pause(delay)
}
Then you can use the retrier this way:
const result = await retrier(
() => tryOperation(),
{ attempts: 5, delay: 500 }
)
As much as this works, there are a few things I don't like with this approach:
- You have little control on what's happening inside of the for loop (how many time did it take to succeed?)
- You have to pass the operation as a parameter which I think feel a bit weird
- Any custom logic you would need to get executed within the loop (for instance if you have several operations) will have to get into the
tryOperation
function
Of course you could avoid creating a retrier
function and just duplicate this for loop everywhere. But with a more and more complicated code inside of the loop, or with break
or continue
statements, it would become really complex.
Generator functions
Another way to implement this is to use an Async Generator. But first, let's have a look at what is a Generator.
A Generator function is a function (what a surprise) that returns a Generator (big brain time). A Generator yields
values which you can iterate on, using a for of
loop for instance.
The point of a Generator is that it can build values when you need them, instead of building an array, then iterating on it for instance. Consider the following example:
// function* is the way to declare a Generator
function* count() {
let count = 0
// yield allows you to "generate" a value
while(true) yield i++
}
If you use that Generator, you can iterate forever, with a count that increases up to Infinity. Without the need to generate all numbers beforehand!
for (const index of count()) { console.log(index) }
Async Generators
Now what's the difference with an Async Generator? Well... It's a Generator, but async! It's all you have to know about it, really.
You will declare it the same way, but with async before the function
keyword, then use await
in the for loop declaration.
Here is the retrier implemented using an Async Generator:
async function* retrier({ attempts = Infinity, delay = 100 }) {
for (let i = 0; i < attempts; i++) {
yield i
await pause(delay)
}
}
Now, if you want to use this, all you have to do is to use a for await loop:
for await (const _ of retrier({ attempts: 5, delay: 500 })) {
// This gets executed every 500ms
// And up to 5 times!
const result = await tryOperation()
if (result) break
}
As much as I agree that it does not change "much", I think this code is easier to approach and reason about as you keep the loop, which we are used to in JavaScript.
Photo by Jayphen Simpson on Unsplash
Top comments (0)