Error handling Scala : Future For Comprehension

2019-03-12 10:23发布

问题:

I want to do error handling in my play scala web application.

My application talks to the data base to fetch some rows, it follows following flow.

  1. First call to db to fetch some data
  2. Use the data in first call to fetch other data from db
  3. Form a response using the data received from last two db calls.

Below is my pseudocode.

 def getResponse(name: String)
      (implicit ctxt: ExecutionContext): Future[Response] = {
    for {
        future1 <- callFuture1(name)
        future2 <- callFuture2(future1.data)
        future3 <- callFuture3(future1.data, future2.data)
    }  yield future3
  }

Every method in the comprehension above returns a future, the signature of these methods are as below.

private def callFuture1(name: String)
  (implicit ctxt: ExecutionContext): Future[SomeType1] {...}

private def callFuture2(keywords: List[String])
  (implicit ctxt: ExecutionContext): Future[SomeType2] {...}

private def callFuture3(data: List[SomeType3], counts: List[Int])
  (implicit ctxt: ExecutionContext): Future[Response] {...}

How shall I do error/failure handling, in the following situation

  • When callFuture1 fails to fetch data from database. I want to return a appropriate error response with error message. Since callFuture2 only gets executed after callFuture1. I dont want to execute callFuture2 if callFuture1 has failed/erred and would want to return error message immediately. (Same thing for callFuture2 and callFuture3)

--edit--

I am trying to return an appropriate Error Response from getResponse() method, when either of the callFuture fails and not proceed to subsequent futureCalls.

I tried the following, based on Peter Neyens answer, but gave me an runtime error..

 def getResponse(name: String)
      (implicit ctxt: ExecutionContext): Future[Response] = {
    for {
        future1 <- callFuture1(name) recoverWith {
         case e:Exception => return Future{Response(Nil,Nil,e.getMessage)}
        }
        future2 <- callFuture2(future1.data)
        future3 <- callFuture3(future1.data, future2.data)
    }  yield future3
  }

Runtime error i get

ERROR] [08/31/2015 02:09:45.011] [play-akka.actor.default-dispatcher-3] [ActorSystem(play)] Uncaught error from thread [play-akka.actor.default-dispatcher-3] (scala.runtime.NonLocalReturnControl)
[error] a.a.ActorSystemImpl - Uncaught error from thread [play-akka.actor.default-dispatcher-3]
scala.runtime.NonLocalReturnControl: null

回答1:

You could use the Future.recoverWith function, to customize the exception if the Future failed.

val failed = Future.failed(new Exception("boom"))
failed recoverWith {
  case e: Exception => Future.failed(new Exception("A prettier error message", e)
}

This will result in a slightly uglier for comprehension :

for {
  future1 <- callFuture1(name) recoverWith {
               case npe: NullPointerException =>
                 Future.failed(new Exception("how did this happen in Scala ?", npe))
               case e: IllegalArgumentException =>
                 Future.failed(new Exception("better watch what you give me", e))
               case t: Throwable =>
                 Future.failed(new Exception("pretty message A", t))
             }
  future2 <- callFuture2(future1.data) recoverWith {
               case e: Exception => Future.failed(new Exception("pretty message B", e))
             }
  future3 <- callFuture3(future1.data, future2.data) recoverWith {
               case e: Exception => Future.failed(new Exception("pretty message C", e))
             }
} yield future3

Note that you could also define your own exceptions to use instead of Exception, if you want to add more information than just an error message.

If you don't want fine grained control to set a different error message depending on the Throwable in the failed Future (like with callFuture1), you could enrich Future using an implicit class to set a custom error message somewhat simpler:

implicit class ErrorMessageFuture[A](val future: Future[A]) extends AnyVal {
  def errorMsg(error: String): Future[A] = future.recoverWith {
    case t: Throwable => Future.failed(new Exception(error, t))
  }
}

Which you could use like :

for {
  future1 <- callFuture1(name) errorMsg "pretty A"
  future2 <- callFuture2(future1.data) errorMsg "pretty B"
  future3 <- callFuture3(future1.data, future2.data) errorMsg "pretty C"
} yield future3

In both cases, using errorMsg or recoverWith directly, you still rely on Future, so if a Future fails the following Futures will not be executed and you can directly use the error message inside the failed Future.

You didn't specify how you would like to handle the error messages. If for example you want to use the error message to create a different Response you could use recoverWith or recover.

future3 recover { case e: Exception =>
  val errorMsg = e.getMessage
  InternalServerError(errorMsg)
}


回答2:

Say future1, future2 and future3 throw Throwable exceptions named Future1Exception, Future2Exception and Future3Exception, respectively. Then you can return appropriate error Response from getResponse() method as follows:

def getResponse(name: String)
             (implicit ctxt: ExecutionContext): Future[Response] = {
  (for {
    future1 <- callFuture1(name)
    future2 <- callFuture2(future1.data)
    future3 <- callFuture3(future1.data, future2.data)
  } yield future3).recover {
    case e: Future1Exception =>
      // build appropriate Response(...)

    case e: Future2Exception =>
      // build appropriate Response(...)

    case e: Future3Exception =>
      // build appropriate Response(...)
  }
}

According to documentation Future.recover

Creates a new future that will handle any matching throwable that this future might contain.