This seems to be the intended, documented behaviour of cats-effect. The ZIO equivalent to cancel is interrupt, and it returns an Exit type that will allow you to handle all possible cases (success, error, interruption) very easily, so if possible, I'd encourage you to just switch to ZIO.

But if you need to use cats-effect, the best that I could come up with (and it's not very good) is to use a Deferred:

    def catchCancel[F[_]: Concurrent, A](io: F[A]): F[(F[Unit], F[Either[Option[Throwable], A]])] =
      for {
        deferred <- Deferred[F, Either[Option[Throwable], A]]
        fiber <- (for {
          a <- Concurrent[F].unit.bracketCase(_ => io) {
            case (_, ExitCase.Completed) => Concurrent[F].unit
            case (_, ExitCase.Error(e)) => deferred.complete(Left(Some(e))).as(())
            case (_, ExitCase.Canceled) => deferred.complete(Left(None))
          _ <- deferred.complete(Right(a))
        } yield ()).start
      } yield (fiber.cancel, deferred.get)

And you can use it like so:

    val test = for {
      x <- catchCancel[IO, Nothing](IO.never)
      (cancel, getResult) = x
      _ <- cancel
      result <- getResult
      _ <- IO(println(s"Finished $result"))
    } yield ()

But seriously, why would you inflict this onto yourself? Just switch to ZIO, it fixes this problem and a boatload of others.

Related Query

More Query from same tag