Future.successful(Failure(Exception)) vs just Future.failed()

Viewed 310

I am trying to understand the difference why someone will use Future.successful(Failure(ServiceException("error"))) rather than Future.failed(ex) in Scala.

Future.successful(
  Failure(
    ServiceException(ErrorTypes.Validation, "Value should not be more than 30")
  )
)
2 Answers

It's hard to say why the author did what they did, I can guess a couple of reasons:

  1. They are not aware that you can Future.fail to return a failed future. This may happen because the signature of future doesn't convey the failure path explicitly in the type. When you see Future[A], you may think this computation must succeed in the future

  2. They wanted to be explicit about failure. When I declare Future[Try[A]], I am forcing the caller to deal with the fact that can't directly access A, but have to "jump another hop" to get inside for A. Although this is delicate, as one can easily write:

     val f: Future[Try[String]] = ???
     f.map(_.map(str => // stuff with string))
    

    But perhaps the signature forces the caller to realize the possibility of failure. Although Try is pretty limiting in it's expressiveness as well, as it can only return a Throwable on the failure path. I find Future[Either[E, A]] to be more expressive in that regards, which allows you to build your own failure hierarchy and encode it in the type.

There is an ever lasting debate regarding unary functor IO types (Future[A]) vs bi-functor IO types (IO[E, A], i.e. ZIO). I am in favor of bi-functor and have been using it solely recently, and I find it to be really time-saving when I don't have to dig into the implementation to understand the possible failures of a computation.

I would like to add on top of the great answer by @YuvalItzchakov, that writing code as in your question complicates the code and makes it unnatural. For example, let's assume that this future normally an Int, something like:

val succeeded = true // or any other computation
val future = if(succeeded) {
  Future.successful(42)
} else {
  Future.successful(Failure(new Exception("some failure text")))
}

Then first, the type of future is Future[Any], because Any is the lowest common root of Int and Failure, which is not convenient. But then, to map on it, you need to do:

future.map {
  case i: Int =>
    println(i)
  case Failure(ex) =>
    println(ex.getMessage)
} recover {
  case NonFatal(ex) =>
    println(s"recover ${ex.getMessage}")
}

Which feels weird to pattern match Int and Failure in the same match case. If you do want to pattern match Success and Failure, we had to do:

val succeeded = true // or any other computation
val future = if(succeeded) {
  Future.successful(Success(42))
} else {
  Future.successful(Failure(new Exception("some failure text")))
}

But that is already pretty clear as redundancy, right? No one will call Future.successful(Success(42)). So I think that it is pretty clear to use Future.failed(new Exception("some failure text")).

Related