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 Futures, 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.