I recently created a wonderful bug.
Have a look at this code and tell me: Do you need to look out for exceptions?
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.util.{Failure, Success}
object Main extends App {
val myFuture: Future[Int] = for {
foo <- doFoo()
bar <- doBar()
} yield foo + bar
def doFoo(): Future[Int] = ???
def doBar(): Future[Int] = ???
myFuture onComplete {
case Success(value) => println(s"It's a $value")
case Failure(exception) => println(s"Something went wrong: $exception")
}
}
Nahhh, probably not, because
Future{
throw new Throwable
}
is the same as
Future.apply(throw new Throwable)
is the same as
Future.unit.map(_ => throw new Throwable)
is the same as
Future.successful(()).transform(_ map (_ => throw new Throwable))
and this transform
is defined as
def transform[S](f: Try[T] => Try[S])(implicit executor: ExecutionContext): Future[S]
and therefore the map
is defined as
def map[U](f: T => U): Try[U]
and Try
has the constructor
object Try {
/** Constructs a `Try` using the by-name parameter. This
* method will ensure any non-fatal exception is caught and a
* `Failure` object is returned.
*/
def apply[T](r: => T): Try[T] =
try Success(r) catch {
case NonFatal(e) => Failure(e)
}
}
and there we finally have our try-catch-block! So we don’t have to care about exceptions in Future
s, right?
The Catch
While the conclusion is correct, and exceptions inside futures will be caught, not all the code is inside a future!
The for
comprehension de-sugared is the same as
doFoo().flatMap(foo => doBar().map(bar => foo + bar))
and although doFoo()
returns a Future
, there can be parts that are not inside that Future
:
def doFoo(): Future[Int] = {
throw new Throwable
Future(3)
}
BÄM! Now there’s no try
around the throw
and the Throwable
will bubble to the top and kill your program.
The Solution
Obviously, when returning a Future
you shouldn’t throw exceptions. But if you’re the caller and don’t have any influence on this, this will save you:
val myFuture: Future[Int] = for {
_ <- Future.unit
foo <- doFoo()
bar <- doBar()
} yield foo + bar
It’s pretty much the same way the standard library does it: Start with something successful and map
your way from there!
De-sugared, this would be
Future.unit.flatMap(_ => doFoo().flatMap(foo => doBar().map(bar => foo + bar)))
What a mess, just to be on the safe side.