0

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?

Robo
  • 4,588
  • 7
  • 40
  • 48

2 Answers2

1

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)
    }
 }
Dima
  • 39,570
  • 6
  • 44
  • 70
0

I tend to agree that it seems a bit convoluted, but for the sake of the exercise, I believe there are a couple of things that don't quite click.

The first one is the fact that you are throwing the Exception instead of capturing it as part of the semantics of Future. ie. You should change your method doException from:

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

To:

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

The second bit that is not quite right, would be the recovery of the Exception. When you call recoverWith on an EitherT, you're defining a partial function from the Left of the EitherT to another EitherT. In your case, that'd be:

ServiceError => AsyncResult[Int]

If what you want is to recover the failed future, I think you'll need to explicitly recover on it. Something like:

AsyncResult {
  result.value.recover {
    case _: Throwable => {
      println("recovered exception")
      Right(99)
    }
  }
}

If you really want to use recoverWith, then you could write this instead:

 AsyncResult {
    result.value.recoverWith {
      case _: Throwable =>
        println("recovered exception")
        Future.successful(Right(99))
    }
  }
hasumedic
  • 2,139
  • 12
  • 17