Search code examples
scalascala-cats

Unable to recover exception in Future in Scala


The following Scala code uses cats EitherT to wrap results in a Future[Either[ServiceError, T]]:

package com.example

import com.example.AsyncResult.AsyncResult
import cats.implicits._

import scala.concurrent.ExecutionContext.Implicits.global

class ExternalService {
  def doAction(): AsyncResult[Int] = {
    AsyncResult.success(2)
  }

  def doException(): AsyncResult[Int] = {
    println("do exception")
    throw new NullPointerException("run time exception")
  }
}

class ExceptionExample {
  private val service = new ExternalService()

  def callService(): AsyncResult[Int] = {
    println("start callService")
    val result = for {
      num <- service.doException()
    } yield num

    result.recoverWith {
      case ex: Throwable =>
        println("recovered exception")
        AsyncResult.success(99)
    }
  }
}

object ExceptionExample extends App {
  private val me     = new ExceptionExample()
  private val result = me.callService()
  result.value.map {
    case Right(value) => println(value)
    case Left(error)  => println(error)
  }
}

AsyncResult.scala contains:

package com.example

import cats.data.EitherT
import cats.implicits._

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

object AsyncResult {
  type AsyncResult[T] = EitherT[Future, ServiceError, T]

  def apply[T](fe: => Future[Either[ServiceError, T]]): AsyncResult[T]          = EitherT(fe)
  def apply[T](either: Either[ServiceError, T]): AsyncResult[T]                 = EitherT.fromEither[Future](either)
  def success[T](res: => T): AsyncResult[T]                                     = EitherT.rightT[Future, ServiceError](res)
  def error[T](error: ServiceError): AsyncResult[T]                             = EitherT.leftT[Future, T](error)
  def futureSuccess[T](fres: => Future[T]): AsyncResult[T]                      = AsyncResult.apply(fres.map(res => Right(res)))
  def expectTrue(cond: => Boolean, err: => ServiceError): AsyncResult[Boolean]  = EitherT.cond[Future](cond, true, err)
  def expectFalse(cond: => Boolean, err: => ServiceError): AsyncResult[Boolean] = EitherT.cond[Future](cond, false, err)
}

ServiceError.scala contains:

package com.example

sealed trait ServiceError {
  val detail: String
}

In ExceptionExample, if it call service.doAction() it prints 2 as expected, but if it call service.doException() it throws an exception, but I expected it to print "recovered exception" and "99".

How do I recover from the exception correctly?


Solution

  • That is because doException is throwing exception inline. If you want to use Either, you have to return Future(Left(exception)) rather than throwing it.

    I think, you are kinda overthinking this. It does not look like you need Either here ... or cats for that matter.

    Why not do something simple, like this:

     class ExternalService {
       def doAction(): Future[Int] = Future.successful(2)
    
       def doException(): AsyncResult[Int] = {
         println("do exception")
         Future.failed(NullPointerException("run time exception")) 
         // alternatively: Future { throw new NullPointerExceptioN() }
     }
    
    
     class ExceptionExample {
       private val service = new ExternalService()
    
       def callService(): AsyncResult[Int] = {
         println("start callService")
           val result = for {
             num <- service.doException()
         } yield num
         // Note: the aboive is equivalent to just
         // val result = service.doException
         // You can write it as a chain without even needing a variable:
         // service.doException.recover { ... }
    
         result.recover { case ex: Throwable =>
           println("recovered exception")
           Future.successful(99)
        }
     }