Getting started

Let’s start with a realistic example.

In order to provide business value to our stakeholders, we need to download a textual description of a cat gif.

Unfortunately we have to do this over a flaky network connection, so there’s a high probability it will fail.

We’ll be working with the cats-effect IO monad, but any monad will do.

import cats.effect.IO

val httpClient = util.FlakyHttpClient()
// httpClient: util.FlakyHttpClient = FlakyHttpClient()

val flakyRequest: IO[String] = IO {
  httpClient.getCatGif()
}
// flakyRequest: IO[String] = Delay(thunk = <function0>)

To improve the chance of successfully downloading the file, let’s wrap this with some retry logic.

We’ll add dependencies on the core and cats-effect modules:

val catsRetryVersion = "3.1.0"
libraryDependencies += "com.github.cb372" %% "cats-retry" % catsRetryVersion,

(Note: if you’re using Scala.js, you’ll need a %%% instead of %%.)

First we’ll need a retry policy. We’ll keep it simple: retry up to 5 times, with no delay between attempts. (See the retry policies page for information on more powerful policies).

import retry._

val retryFiveTimes = RetryPolicies.limitRetries[IO](5)
// retryFiveTimes: RetryPolicy[IO] = RetryPolicy(
//   decideNextRetry = retry.RetryPolicy$$$Lambda$11987/1347236281@5dcb8c0b
// )

We’ll also provide an error handler that does some logging before every retry. Note how this also happens within whatever monad you’re working in, in this case the IO monad.

import cats.effect.IO
import scala.concurrent.duration.FiniteDuration
import retry._
import retry.RetryDetails._

val httpClient = util.FlakyHttpClient()
// httpClient: util.FlakyHttpClient = FlakyHttpClient()

val flakyRequest: IO[String] = IO {
  httpClient.getCatGif()
}
// flakyRequest: IO[String] = Delay(thunk = <function0>)

val logMessages = collection.mutable.ArrayBuffer.empty[String]
// logMessages: collection.mutable.ArrayBuffer[String] = ArrayBuffer(
//   "Failed to download. So far we have retried 0 times.",
//   "Failed to download. So far we have retried 1 times.",
//   "Failed to download. So far we have retried 2 times.",
//   "Failed to download. So far we have retried 3 times."
// )

def logError(err: Throwable, details: RetryDetails): IO[Unit] = details match {

  case WillDelayAndRetry(nextDelay: FiniteDuration,
                         retriesSoFar: Int,
                         cumulativeDelay: FiniteDuration) =>
    IO {
      logMessages.append(
        s"Failed to download. So far we have retried $retriesSoFar times.")
    }

  case GivingUp(totalRetries: Int, totalDelay: FiniteDuration) =>
    IO {
      logMessages.append(s"Giving up after $totalRetries retries")
    }

}

// Now we have a retry policy and an error handler, we can wrap our `IO` inretries.

import cats.effect.unsafe.implicits.global

val flakyRequestWithRetry: IO[String] =
  retryingOnAllErrors[String](
    policy = RetryPolicies.limitRetries[IO](5),
    onError = logError
  )(flakyRequest)
// flakyRequestWithRetry: IO[String] = FlatMap(
//   ioe = FlatMap(
//     ioe = Attempt(ioa = Delay(thunk = <function0>)),
//     f = retry.package$RetryingOnSomeErrorsPartiallyApplied$$Lambda$12035/340735956@59d52751
//   ),
//   f = cats.StackSafeMonad$$Lambda$11991/1077262642@58ff43b4
// )

// Let's see it in action.

flakyRequestWithRetry.unsafeRunSync()
// res2: String = "cute cat gets sleepy and falls asleep"

logMessages.foreach(println)
// Failed to download. So far we have retried 0 times.
// Failed to download. So far we have retried 1 times.
// Failed to download. So far we have retried 2 times.
// Failed to download. So far we have retried 3 times.

Next steps: