无法在 Scala 的 Future 中恢复异常

Unable to recover exception in Future in Scala

以下 Scala 代码使用 cat EitherT 将结果包装在 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 包含:

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 包含:

package com.example

sealed trait ServiceError {
  val detail: String
}

ExceptionExample中,如果它调用service.doAction(),它会按预期打印2,但如果它调用service.doException(),它会抛出异常,但我预计它会打印"recovered exception" 和 "99".

如何正确地从异常中恢复?

那是因为 doException 正在抛出内联异常。如果你想使用Either,你必须return Future(Left(exception))而不是扔掉它。

我觉得,你有点想多了。看起来您在这里不需要 Either ... 或 cats

为什么不做一些简单的事情,像这样:

 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)
    }
 }

我倾向于认为它看起来有点令人费解,但为了练习,我相信有几件事不太清楚。

第一个是您抛出异常而不是将其捕获为 Future 语义的一部分。 IE。您应该将方法 doException 更改为:

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

收件人:

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

第二个不太正确的地方是异常的恢复。当您在 EitherT 上调用 recoverWith 时,您正在定义从 EitherTLeft 到另一个 EitherT 的偏函数。在你的情况下,那将是:

ServiceError => AsyncResult[Int]

如果你想要恢复失败的未来,我认为你需要明确地恢复它。类似于:

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

如果你真的想使用recoverWith,那么你可以这样写:

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