 
Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
Last updated: April 15, 2025
Error handling is a crucial aspect of any programming language, and Scala offers multiple ways to manage it. Among the most commonly used tools for error handling is the Either type, along with alternatives like Try. However, using Either directly can become complex when dealing with nested types such as Future, IO, or other effectful contexts.
In this tutorial, we’ll explore how EitherT simplifies and enhances error handling in Scala, making it more composable and easier to work with in such scenarios.
As mentioned above, Either is one of Scala’s most common error-handling techniques.
Let’s define a simple scenario where we use Either to handle errors:
def getUserProfile(userId: String): Either[String, User] = ???
def calculateDiscount(user: User): Either[String, Double] = ???
def placeOrder(itemId:String, discount:Double, user: User): Either[String, String] = ???
def performAction(userId: String, itemId: String): Either[String, String] = for {
  user <- getUserProfile(userId)
  discount <- calculateDiscount(user)
  orderId <- placeOrder(itemId, discount, user)
} yield orderIdIn this example, we have three functions that can potentially fail, so each returns an Either to represent a success or failure. Using a for-comprehension, we can elegantly compose these functions into a single workflow, making it easy to perform the entire action while handling any possible errors along the way.
Everything works fine and elegantly so far. However, since these operations involve database and network calls, we need to make them asynchronous using Future, IO, or similar. Let’s rewrite the code to use Future as the effect type to handle these async operations:
def getUserProfile(userId: String): Future[Either[String, User]] = ???
def calculateDiscount(user: User): Future[Either[String, Double]] = ???
def placeOrder(itemId:String, discount:Double, user: User): Future[Either[String, String]] = ???In this case, we are dealing with nested monads. One represents the asynchronous nature using Future, and another represents potential failures using Either.
Since the methods now return a Future, we can’t compose these functions using a single for-comprehension. Let’s look at a possible way to handle this:
def performAction(userId: String, itemId: String): Future[Either[String, String]] = {
    for {
      userEither <- getUserProfile(userId)
      result <- userEither match {
        case Left(error) => Future.successful(Left(error))
        case Right(user) =>
          for {
            discountEither <- calculateDiscount(user)
            orderResult <- discountEither match {
              case Left(error) => Future.successful(Left(error))
              case Right(discount) => placeOrder(itemId, discount, user)
            }
          } yield orderResult
      }
    } yield result
  }As we can see, this approach significantly increases the amount of code needed to handle errors properly. Furthermore, as the number of such functions grows, the complexity and verbosity of the code can escalate even further, making it harder to read and maintain. This is where EitherT comes to the rescue.
EitherT is a monad transformer from the Cats library that enhances the capabilities of the Either type by allowing it to be combined with other monads such as Future, Option, IO, and so on. Instead of manually unwrapping and managing multiple monads, monad transformers allow us to flatten these layers into a single structure that can be easily composed.
In this section, we’ll explore how to utilize this type to simplify the previous error-handling code.
To use EitherT, we should add the cats-core library to the build.sbt file:
libraryDependencies += "org.typelevel" %% "cats-core" % "2.12.0"With this, we’ll be able to import the EitherT type from the cats.data package.
The type signature for EitherT is represented as EitherT[F[_], L, R], where F is a type constructor that indicates the outer monad such as Future, IO, and so on. In this signature, L represents the type of the left value similar to the Left in Either, often used for error messages. Similarly, R represents the type of the right value which is analogous to the Right in Either, indicating successful outcomes.
Let’s look at how we can create an instance of EitherT. In this section, we’ll use Try for the type F. But it works the same way for Future, IO, and more:
val opValue: Try[Either[String, Int]] = Try(Right(100))
val response: EitherT[Try, String, Int] = EitherT(opValue)As EitherT is a simple case class, we can just wrap the value to create the instance.
If we already know the value is Right, we can directly lift it into EitherT using the liftF() or right() functions:
val num1: EitherT[Try, String, Int] = EitherT.right(Try(100))
val num2: EitherT[Try, String, Int] = EitherT.liftF(Try(2))Similarly, we can use the left() function to lift an error into EitherT.
Additionally, EitherT provides convenient methods like fromOption() and fromEither(), which allow to lift an Option or Either directly into an EitherT instance without manually wrapping them.
Since it is a Monad, we can use functions such as map, flatMap, and so on on the EitherT instance. Similar to Either, these functions operate directly on the R type, without needing to manually handle the outer effect type F. This simplifies working with asynchronous or effectful computations. Furthermore, we can use the function leftMap() to work with the L type. To extract the underlying value from an EitherT instance, we can use the value() function:
val opValue: Try[Either[String, Int]] = Try(Right(100))
val response: EitherT[Try, String, Int] = EitherT(opValue)
val mappedValue: EitherT[Try, String, Int] = response.map(_ * 5)
val underlying: Try[Either[String, Int]] = mappedValue.value
underlying shouldBe Try(Right(500))
val failureCase: EitherT[Try, String, Int] =  EitherT(Try(Left("invalid number!")))
response.leftMap(_.toUpperCase).value shouldBe Try(Left("INVALID NUMBER!"))The bimap() function allows us to apply separate functions to handle both the success and error cases:
val eithert: EitherT[Try, String, Int] =  EitherT(Try(Right(100)))
val biMappedRes = eithert.bimap(e => e.toUpperCase, s => s * 5)Since it is a monad, we can use for-comprehension to easily compose multiple instances of EitherT:
val num1: EitherT[Try, String, Int] =  EitherT(Try(Right(100)))
val num2: EitherT[Try, String, Int] =  EitherT(Try(Right(2)))
val divRes: EitherT[Try, String, Int] = for {
  n1 <- num1
  n2 <- num2
  div = n1 / n2
} yield div
divRes.value shouldBe Try(Right(50))As shown here, working with the right-side value of the EitherT instance is straightforward and seamless. This allows us to focus on the logic of our code, rather than on how to manage nested monads.
To rewrite the previously discussed complex scenario using EitherT, we don’t need to change the signature of our methods that retrieve data. Therefore, the functions we defined previously will remain unchanged:
def getUserProfile(userId: String): Future[Either[String, User]] = ???
def calculateDiscount(user: User): Future[Either[String, Double]] = ???
def placeOrder(itemId:String, discount:Double, user: User): Future[Either[String, String]] = ???Now, we can wrap each of these function calls in EitherT and use for-comprehension to compose them:
def performAction(userId: String, itemId: String): Future[Either[String, String]] = {
  (for {
    user <- EitherT(getUserProfile(userId))
    discount <- EitherT(calculateDiscount(user))
    orderId <- EitherT(placeOrder(itemId, discount, user))
  } yield orderId).value
}As we can see, the code is greatly simplified. It now closely resembles the original version that didn’t involve Future.
In this article, we explored various approaches to error handling in Scala, starting with the basic use of Either for handling errors in synchronous operations. We then introduced effects like Future or IO and saw how they add complexity and boilerplate when managing asynchronous operations combined with error handling. This led to the introduction of EitherT from the Cats library, which significantly simplifies the code when dealing with nested effects, like Future[Either].
By using EitherT, we can maintain clean, composable, and concise error-handling logic, even in more complex scenarios. Additionally, we looked at various useful functions provided by EitherT that allow us to handle both success and failure cases effectively.