DEV Community

loading...
Cover image for Implement a retrier using Async Generator

Implement a retrier using Async Generator

qmenoret profile image Quentin Ménoret Updated on ・3 min read

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))
}
Enter fullscreen mode Exit fullscreen mode

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)
  }
Enter fullscreen mode Exit fullscreen mode

Then you can use the retrier this way:

const result = await retrier(
  () => tryOperation(),
  { attempts: 5, delay: 500 }
)
Enter fullscreen mode Exit fullscreen mode

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++
}
Enter fullscreen mode Exit fullscreen mode

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) }
Enter fullscreen mode Exit fullscreen mode

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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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

Discussion (0)

pic
Editor guide