## 1. Introduction

In this tutorial, we’ll delve into the foundational aspects of error raising and handling using higher-kinded types.

Additionally, we’ll learn the essential capabilities and applications of type classes like *ApplicativeError* and *MonadError* from the Cats library.

We’ll also see how these concepts are applied to one of the core data structures in the Cats Effect library, the *IO* monad.

## 2. Raising and Handling Errors and Higher-Order Types

Let’s say we are trying to create a simplistic calculating service, where unknown errors could occur. Below, *F[???]* represents this uncertainty:

```
trait Calculator[F[_]] {
def calculate(f: => Int): F[???]
}
```

We can make use of the *Either[+A, +B]* type from the Scala standard library to encompass potential error cases of type *E* and the successfully returned value of *Int*. With the understanding that *calculate* could potentially raise an error, our abstract data type *Calculator[F[_]]* takes this from:

```
trait Calculator[F[_], E] {
def calculate(f: => Int): F[Either[E, Int]]
}
```

This way of dealing with errors is straightforward: the error is one of two possible values. However, this approach has a few shortcomings. First, it leads to a pretty cluttered code. Secondly, we may need to write a lot of additional code to facilitate transformations of the values, as the value is wrapped in both *Either* and *F. *In this case one can also employ *EitherT* monad transformer*. *However, it’s not always necessary to carry errors all along the way. **Instead, we can remain within the context of F[_] and delegate the responsibility of raising and handling errors to the implementation:**

```
trait Calculator[F[_]] {
def calculate(f: => Int): F[Int]
}
```

Such approach is implemented in the Tagless Final design pattern.

## 3. Tools for Error Handling From Cats and Cats Effect

Cats offers functionality for error handling through type classes, such as *ApplicativeError* and *MonadError*. They become accessible when implicit instances of these type classes are available for the effect type *F*:

```
import cats.syntax.applicative._
import cats.{Applicative, ApplicativeError}
class CalculatorImpl[F[_]]()(implicit m: ApplicativeError[F, Throwable]) extends Calculator[F]
```

Cats Effect provides instances of *ApplicativeError* and *MonadError *for* IO *and additionally there’re a lot of methods for error handling available for *IO* directly (with the names similar to the ones from *ApplicativeError* and *MonadError*)*.*

### 3.1. Adding Project Dependency

Before we begin, let’s include a dependency for the Cats Effect library:

```
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-effect" % "3.5.2"
)
```

In the majority of the examples, we’ll utilize Cats library functionality, which is available through this import along with Cats Effect functionality.

### 3.2. On *applicativeError* and *monadError* Syntax

We’ll employ the *applicativeError* and *monadError* syntax to make error handling resemble a method call. This approach is similar to how we utilize syntax for *Functor* and *FlatMap*, enabling postfix calls such as *f.map *and *f.flatMap *through the import of implicits:

```
import cats.syntax.functor._
import cats.syntax.flatMap._
```

Similarly, to invoke methods such as *raiseError*, *handleError*, and *ensure* for our effects (e.g. *f.handleError*), we add:

```
import cats.syntax.applicativeError._
import cats.syntax.monadError._
```

### 3.3. Raising Errors and Understanding *ApplicativeError[F[_], E]*

** ApplicativeError[F[_], E] allows to raise and handle errors for the higher-order type F[_]**. As an applicative functor, this abstraction enables lifting a value into the context of

*F*. When an implicit instance of

*ApplicativeError*is available for our effect

*F*, such as

*IO*, our effect gains the capabilities of

*ApplicativeError,*e.g., to raise an error:

`def raiseError[A](e: E): F[A]`

The question might arise: **if we operate on a value of type E and the returned value is F[A], how can we retain error information?**

**To achieve this, we utilize subtyping.**In particular, the implementation of

*raiseError*for

*IO*resolves this concern:

`def raiseError[A](t: Throwable): IO[A] = Error(t)`

The *Error(t)* resides in the *cats.effect.IO *and it is essentially a failed effect *IO[Nothing]*:

```
private[effect] final case class Error(t: Throwable) extends IO[Nothing] {
def tag = 1
}
```

Now we can implement the *Calculator[F[_]]*:

```
import cats.syntax.applicative._
import cats.{Applicative, ApplicativeError}
import scala.util.{Try, Success, Failure}
class CalculatorImpl[F[_] : Applicative]()(implicit m: ApplicativeError[F, Throwable]) extends Calculator[F] {
override def calculate(f: => Int): F[Int] =
Try(f) match {
case Success(res) => res.pure[F]
case Failure(_) => m.raiseError[Int](new RuntimeException("Calculation failed"))
}
}
```

Henceforth, we’ll have the capability to raise errors while staying within the context of the effect *F*.

### 3.4. Handling Errors With *ApplicativeError[F[_], E]*

**Next, what will we do with the error? We can handle it by returning some other value right away with handleError, so that the new value is encapsulated within the context of F**:

`def handleError[A](fa: F[A])(f: E => A): F[A]`

We can just discard the error and return some default value employing the function *Throwable => Int*:

`c.calculate("5 * 3".toInt).handleError(_ => -1)`

However, as it’s not recommended to ignore the error, at the very least, we should log it. Usually, logging involves using some effect, such as *Logger* from log4cats. Hence, we’ll utilize the counterpart of *handleError*, namely *handleErrorWith*:

`def handleErrorWith[A](fa: F[A])(f: E => F[A]): F[A]`

And now we can handle the error better:

`c.calculate("5 * 3".toInt).handleErrorWith(e => logger.error(s"error calculating the value: ${e.getMessage}").map(_ => -1))`

### 3.5. *ApplicativeError* Toolkit

There are numerous other error-handling methods accessible within *ApplicativeError*, which are extensively employed in the Cats ecosystem. Several of these methods produce a new value of *F[_]*, as we saw above, in one way or another.

With *attempt* we produce *F[Either[E, A]]* out of *F[A]* so that *F[Left[E, A]]* returns in case of error and *F[Right[E, A]]* otherwise:

`def attempt[A](fa: F[A]): F[Either[E, A]]`

The methods *recover* and *recoverWith* allow to handle only a specific subset of errors:

```
def recover[A](fa: F[A])(pf: PartialFunction[E, A]): F[A]
def recoverWith[A](fa: F[A])(pf: PartialFunction[E, F[A]]): F[A]
```

Below we return *IO(-1)* in case of only *ArithmeticException*:

```
def calculate(f: => Int): IO[Int] =
IO(f).recover {
case _: ArithmeticException => -1
}
```

With *adaptError* we may skip error handling altogether and simply transform the error or execute some action instead:

`def adaptError[A](fa: F[A])(pf: PartialFunction[E, E]): F[A]`

If the effect *fa* contains the error *E*, *adaptError* catches it and returns the error of the same type *E*. For instance, we can lift the original error to some domain error *CalculationException*:

```
def calculate(f: => Int): IO[Int] =
IO(f).adaptError {
case e: Throwable => new CalculationException(reason = Option(e.getMessage))
}
```

By means of the *onError* method, we can check whether the effect *F[A]* failed and execute some action before rethrowing the error:

`def onError[A](fa: F[A])(pf: PartialFunction[E, F[Unit]]): F[A]`

If the error doesn’t match, it’s simply rethrown. **However, there’s one caveat with IO: an unmatched error of type Throwable remains unhandled, and we can’t handle it further!**

In the following example, we have a callback for *ArithmeticException* inside *onError*, while *NumberFormatException* is left unhandled:

```
def calculate(f: => Int): IO[Int] =
IO(f).onError {
case _: ArithmeticException => logger.error("ArithmeticException")
}.handleError {
case _: ArithmeticException => -1
case _: NumberFormatException => -2
}
```

The line *case _: NumberFormatException => -2* will be unreachable code if *onError* doesn’t catch it, and the following test fails:

`calculate("5 * 3".toInt).unsafeRunSync() should be -2`

Instead, this test just produces the error:

`scala.MatchError: java.lang.NumberFormatException: For input string: "5 * 3" (of class java.lang.NumberFormatException) (of class scala.MatchError)`

Meanwhile, the following assertion passes since *onError* catches *ArithmeticException*:

`calculate(5 / 0).unsafeRunSync() shouldBe -1`

Lastly, there are four utility methods in *ApplicativeError* to raise errors from some Scala’s standard library and Cats types: *fromTry*, *fromEither*, *fromOption* and *fromValidated.*

*fromTry* raises an error in case of *Failure(e) *:

`def fromTry[A](t: Try[A])(implicit ev: Throwable <:< E): F[A]`

*fromEither* raises an error in case of *Left(e) *:

`def fromEither[A](x: Either[E, A]): F[A]`

*fromOption* raises an error in case of *None *:

`def fromOption[A](oa: Option[A], ifEmpty: => E): F[A]`

*fromValidated* raises an error in case of *Invalid(e) *:

`def fromValidated[A](x: Validated[E, A]): F[A]`

These methods return the purified value *F[A] *if they’re invoked on successful values: *Success[A]*, *Right[E, A]*, *Some[A]* and *Valid[A] *respectively.

The* cats.effect.IO*‘s companion object comprises methods with identical names and semantics for our convenience, with the exception of *fromValidated.*

### 3.6. *MonadError*: When and Why?

**Frequently, we may find it necessary to incorporate the capabilities of the Monad type class into our error-handling toolbox for the effect F[_]**.

One such scenario is when we need to return a value if a certain predicate is *true* and raise an error otherwise. Residing in the boundaries of an Applicative Functor, the task becomes non-trivial. However it becomes straightforward with the capabilities of the *FlatMap* or *Monad* type classes:

```
calculator[IO].calculate(5 / 1).flatMap {
case x if predicate => x.pure[IO]
case _ => new RuntimeException("Bad result").raiseError[IO, Int]
}
```

This idea lies at the core of the *ensure* method of *MonadError*:

`def ensure[A](fa: F[A])(error: => E)(predicate: A => Boolean): F[A]`

Another scenario involves unwrapping from *Either[E, A]* within effects of type *F[Either[E, A]]* to obtain refined *F[A], rethrow:*

`def rethrow[A, EE <: E](fa: F[Either[EE, A]]): F[A]`

This is the inverse of the *attempt* we previously saw in *ApplicativeError*:

```
def calculate[F[_] : Applicative](f: => Int)
(implicit ae: ApplicativeError[F, Throwable]): F[Either[Throwable, Int]] = ae.fromTry(Try(f)).attempt
def calculateSafely[F[_]](f: => Int)(implicit me: MonadThrow[F]): F[Int] =
calculate(f).rethrow.handleErrorWith {
case _: ArithmeticException => (-1).pure[F]
case _: NumberFormatException => (-2).pure[F]
}
```

Another valuable method offered by *MonadError* is *redeemWith*, which combines error recovery with binding function to the successful value:

`def redeemWith[A, B](fa: F[A])(recover: E => F[B], bind: A => F[B]): F[B]`

Finally, *MonadError* enables us to return the error value only if an error occurred and return the successful value otherwise. This capability is accessible through *attemptTap*:

```
def attemptTap[A, B](fa: F[A])(f: Either[E, A] => F[B]): F[A]
def calculate[F[_] : Applicative](f: => Int)
(implicit ae: ApplicativeError[F, Throwable]): F[Int] =
ae.fromTry(Try(f))
def calculateOrRaise[F[_]](f: => Int)(implicit me: MonadThrow[F]): F[Int] = calculate(f).attemptTap {
case Left(_) => new RuntimeException("Calculation failed").raiseError[F, Unit]
case Right(_) => ().pure[F]
}
```

## 4. Case Study: Using Domain Errors for Error Handling

Let’s examine the possibility of dealing with only a subset of errors in our code:

```
sealed trait DomainError extends NoStackTrace
case object NotFound extends DomainError
case object InvalidInput extends DomainError
```

**Unfortunately, operating exclusively with these errors isn’t as easy as invoking an implicit m: MonadError[F, DomainError]**. Such an implicit doesn’t exist by default, and we would need to override methods of

*ApplicativeError*,

*FlatMap*, and

*Applicative*(i.e.,

*raiseError*,

*handleErrorWith*,

*pure*,

*flatMap*, and

*tailRecM*) to construct it. This approach results in cumbersome code, and there is an alternative way to constrain the error possibilities using a homemade data type, such as

*RaiseCustomError*:

```
trait RaiseCustomError[F[_]] {
def raiseCustomError[A](e: DomainError): F[A]
}
object RaiseCustomError {
implicit def instance[F[_]](implicit M: MonadError[F, Throwable]): RaiseCustomError[F] =
new RaiseCustomError[F] {
def raiseCustomError[A](e: DomainError): F[A] = M.raiseError(e)
}
}
def serve[F[_] : Applicative](inOpt: Option[String])(implicit R: RaiseCustomError[F]): F[String] =
inOpt match {
case None => R.raiseCustomError(NotFound)
case Some(in) if in.isEmpty => R.raiseCustomError(InvalidInput)
case Some(in) => in.pure[F]
}
```

At present, we employ an implicit instance of *RaiseCustomError *to raise errors and safeguard against arbitrary errors.

This approach involves working solely with the capabilities of the Cats library. **However, with the assistance of libraries like tofu, we can address these and other error-handling challenges which are out of our introductory scope.**

## 5. Common Issues With Error Handling in Cats Effect

In this section, we’ll discuss some common issues with error handling in Cats and Cats Effect.

### 5.1. Instant Handling of Errors

We may have the following function which may raise an error:

`def calculate[F](f: => Int)(implicit ae: ApplicativeError[F, Throwable]): F[Int] = ae.fromTry(Try(f))`

Then, in the event of an error, we can’t assign the effect to some value *res* and handle errors afterward:

```
val res = calculate[IO](5 / 0)
res.handleErrorWith {
case _: ArithmeticException => IO.pure(-1)
case _: NumberFormatException => IO.pure(-2)
}
```

Instead, we need to call *handleErrorWith* right away:

```
val res = calculate[IO](5 / 0).handleErrorWith {
case _: ArithmeticException => IO.pure(-1)
case _: NumberFormatException => IO.pure(-2)
}
```

This applies not only for *IO* but also for other types as well.

### 5.2. Returning the Error of Recovery Method

In the following example, we call *recoverWith *method on the *calculate(f)* effect, but during the recovery itself, another error is thrown. This error is eventually returned by *calculateOrRecover*:

```
def recover[F[_]]()(implicit ae: ApplicativeError[F, Throwable]): F[Int] =
new RuntimeException("Calculation failed").raiseError[F, Int]
def calculateOrRecover[F[_]](f: => Int)(implicit me: MonadThrow[F]): F[Int] = calculate(f).recoverWith {
case _: ArithmeticException => recover()
case _: NumberFormatException => recover()
}
```

Such behavior is actually an adaptation of the error (see *adaptError*). It’s crucial to be aware of this because we lose the initial error information, which is undesirable, particularly when the error isn’t logged.

The general guideline to prevent such pitfalls is to include failure scenarios of the business logic in the tests and not restrict the tests solely to happy paths.

## 6. Conclusion

In this article, we learned the basics of error raising and error handling using *ApplicativeError* and *MonadError* from the Cats library, with applications to abstract effect *F[_]* and to *IO* from Cats Effect.

We explored situations where *ApplicativeError* isn’t enough for our error-handling needs.

Finally, we reviewed the use of custom errors in our code, along with potential pitfalls related to error handling we should be aware of.

As usual, the full code for this article is available over on GitHub.