Is there a good way to have granular control over error handling in `cats.effect.Resource`?

Viewed 89

currently, in order to handle exceptions in different resources differently, I'm doing something akin to this:

def resource1: Resource[F, Service1[F]]
def resource2: Resource[F, Service2[F]]
def resource3: Resource[F, Service3[F]]
def runProgram(Service1[F], Service2[F], Service3[F]): F[ExitCode]

resource1.use(r1 =>
  resource2.use(r2 =>
    resource3.use(r3 =>
      runProgram(r1, r2, r3) //handles its own exceptions
    ).handleErrorWith(e => logger.error(e)("error during service 3 setup").as(ExitCode.Error))
  ).handleErrorWith(e => logger.error(e)("error during service 2 setup").as(ExitCode.Error))
).handleErrorWith(e => logger.error(e)("error during service 1 setup").as(ExitCode.Error))

This works fine, but it's not hard to see why it doesn't scale well. I've tried using .attempt directly on the Resource but it doesn't seem to catch anything. I can imagine figuring out a way to do it in a for comprehension if I use allocate, but I'd rather not If I don't need to. Anyone know a better way?

Edit

So it turns out that the resources I was creating were starting up their own threadpool and doing a lot of impure async processing which caused unstable behavior. It seems that attempt, onError, handleErrorWith, and friends should work just fine for resources that are constructed well.

I am unsure why my nested code seemed to be able to get around the instability, while the other MonadError based solution didn't. Without sufficient evidence to the contrary I am forced to conclude that it must just be lucky timing.

1 Answers

Resource[F, *] is a MonadError just like the underlying F, so you can use all of the associated machinery to factor out the logging code and use convenient syntax:

def withErrorLogging[A](
  resource: Resource[F, A],
  name: String
): Resource[F, A] =
  resource.onError {
    case e => Resource.eval(logger.error(e)(s"error during $name setup"))
  }

(
  withErrorLogging(resource1, "service 1"),
  withErrorLogging(resource2, "service 2"),
  withErrorLogging(resource3, "service 3")
).mapN(runProgram).use(identity).handleError { _ =>
  ExitCode.Error
}
Related