Scala cats exception handling

103 Views Asked by At

Why does it not catch and print the exception msg from calculateTwo? If we make calculateOne throw the exception, the exception is caught and the msg is printed.

  package com.oxo.test
    
    import cats.data.EitherT
    import cats.implicits._
    
    import scala.concurrent.ExecutionContext.Implicits.global
    import scala.concurrent.Future
    
    object FutureError extends App {
    
      try {
        (for {
          result      <- EitherT(calculateOne())
          resultTwo   <- EitherT(calculateTwo())
          resultThree <- EitherT(calculateThree())
          sum          = result + resultTwo + resultThree
        } yield {
          sum
        })
      } catch {
        case e: Exception => println(e.getMessage)
      }
    
      private def calculateOne(): Future[Either[String, Int]] = {
        //throw new RuntimeException("error from one")
        Future.successful(Right(1))
      }
    
      private def calculateTwo(): Future[Either[String, Int]] = {
        //throw new RuntimeException("error from two")
        //Future.successful(Right(2))
      }
    
      private def calculateThree(): Future[Either[String, Int]] = {
        //throw new RuntimeException("error from three")
        Future.successful(Right(3))
      }
    
    }

========= UPDATE =================

From the responses I understand that any exception from first future get thrown right away and any errors from subsequent Futures results in Future.failed. Since we don't want to mix and match with try catch, Is there any elegant way for the below code to continue processing the loop? Irrespective of from where the exception is thrown, handle the error and move on to next element.

 package com.oxo.test
    
    import cats.data.EitherT
    import cats.implicits._
    
    import scala.concurrent.ExecutionContext.Implicits.global
    import scala.concurrent.Future
    
    object FutureError extends App {
    
      val processingList = List(1, 2, 3)
    
      processingList.map { x =>
        val k = (for {
          result      <- EitherT(calculateOne(x))
          resultTwo   <- EitherT(calculateTwo(x))
          resultThree <- EitherT(calculateThree(x))
          sum          = result + resultTwo + resultThree
        } yield {
          sum
        }).value
    
        k.map {
          case Right(value) =>
            println("This gets printed" + value);
          case Left(value)  =>
            println("This error gets printed" + value);
        }.recover {
          case _ =>
            println("processing value" + x);
            println("This error gets printed");
        }
      }
    
      private def calculateOne(input: Int): Future[Either[String, Int]] = {
        throw new RuntimeException("error from one")
        //Future.successful(Right(input + 1))
      }
    
      private def calculateTwo(input: Int): Future[Either[String, Int]] = {
        //throw new RuntimeException("error from two")
        Future.successful(Right(input + 2))
      }
    
      private def calculateThree(input: Int): Future[Either[String, Int]] = {
        //throw new RuntimeException("error from three")
        Future.successful(Right(input + 3))
      }
    
    }
2

There are 2 best solutions below

0
Silvio Mayolo On

EitherT and try ... catch are two completely orthogonal exception handling mechanisms. To deal with result-type-style exceptions like EitherT, we simply call methods on the class, rather than using a special control construct. For instance, getOrElse is a good choice in your situation.

(for {
  result      <- EitherT(calculateOne())
  resultTwo   <- EitherT(calculateTwo())
  resultThree <- EitherT(calculateThree())
  sum          = result + resultTwo + resultThree
} yield {
  sum
}).getOrElse(whateverDefaultValue)

Keep in mind that handling the exception does not excuse you from returning a value of the appropriate type. If sum is an Int, then the result of handling error conditions also needs to be an Int. If you're not returning an Int in that case, you're not handling the exception; you're propagating it.

0
Gaël J On

As it's been said by others: mixing throw/try..catch with Future/Either/EitherT is asking for trouble. Stick to one model or the other.


Now, let's understand what's happening in your code.

Your example can be simplified to the following code which behaves in the same way:

calculateOne()
  .flatMap(_ => calculateTwo())

calculateTwo() will "execute inside" a Future, any exception will be swallowed by the Future and result in a Future.failed. It's roughly the same as if you had written:

calculateOne.flatMap { _ =>
  Future { 
    throw new Exception(...)
  }
}

Which itself is roughly the same as:

calculateOne.flatMap { _ =>
  Future.failed(new Exception(...)) // no throw!
}

The overall expression results in no exception but a Future.failed.


Now, if calculateOne() throws an exception, it's not yet "in" a Future and is roughly the same as:

throw new Exception(...)
calculateTwo() // not even called, dead code

Which is just throwing an exception.

It is NOT the same as:

Future.failed(new Exception())
  .flatMap(_ => calculateTwo())

Which would result in no exception but a failed Future.