简体   繁体   中英

Propagate errors through a chain of Scala futures

Considering a sequence of futures each returning Either[Status, Resp]. How would you propagate error status codes through a for comprehension which is using Future and not Either?

The code bellow does not work, since the parsing exception is not caught by.recover of the last future

The use case is Scala Play ActionRefiners which returns Future[Either[Status, TRequest[A]]].

  def parseId(id: String):Future[Int] = {
    Future.successful(Integer.parseInt(id))
  }

  def getItem(id: Int)(implicit ec: ExecutionContext): Future[Either[Status, String]] =
   Future(Some("dummy res from db " + id)).transformWith {
    case Success(opt) => opt match {
      case Some(item) => Future.successful(Right(item))
      case _ => Future.successful(Left(NotFound))
    }
    case Failure(_) => Future.successful(Left(InternalServerError))
  }

  (for {
    id <- parseId("bad request")
    resp <- getItem(id)
  } yield resp).recover {
    case _:NumberFormatException => Left(BadRequest)
  }

I could move the.recover to parseId, but this makes the for comprehension very ugly - having to treat the Either[Status, id] in the middle

  def parseId(id: String):Future[Either[Status, Int]] = {
    Future.successful(Right(Integer.parseInt(id))).recover {
      case _:NumberFormatException => Left(BadRequest)
    }
  }

Your exception is not caught because you are not throwing it inside the Future : Future.successful is immediately satisfied with the result of the expression you give it, if it throws an exception, it is executed on the current thread.

Try removing the .successful : Future(id.toInt) will do what you want.

Also, I would recommend to get rid of all the Either s: these are highly overrated/overused, especially in the context of Future (that already wrap their result into Try anyhow), and just make the code more complicated and less readable without offering much benefit.

    case class FailureReason(status: Status) 
      extends Exception(status.toString)

    def notFound() = throw FailureReason(NotFound)
    def internalError() = throw FailureReason(InternalError)
    def badRequest() = throw FailureReason(BadRequest)
    

    def parseId(id: String):Future[Int] = Future(id.toInt)
    def getItem(id: Int): Future[String] = Future(Some("dummy"))
       .map { _.getOrElse(notFound) }
       .recover { _ => internalError }

    // this is the same as your for-comprehension, just looking less ugly imo :) 
    parseId("foo").flatMap(getItem).recover { 
      case _: NumberFormatException => badRequest()
    }
    // if you still want `Either` in the end for some reason: 
    .map(Right.apply[Status, String])
    .recover { 
       case _: NumberFormatException => Left(BadRequest) // no need for the first recover above if you do this
       case FailureReason(status) => Left(status)
    }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM