Here’s a little example of how exceptions work with Scala Futures, specifically looking at the onComplete
‘Failure’ case.
In this example I start three Futures that run for different lengths of time, and the shortest-running Future
throws an exception:
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success}
object ScalaFuturesAndExceptions extends App {
val startTime = currentTime
val f1 = Future {
sleep(2000)
1
}
val f2 = Future {
sleep(550)
throw new Exception("Ka-boom!")
2
}
val f3 = Future {
sleep(1000)
3
}
val result = for {
r1 <- f1
r2 <- f2
r3 <- f3
} yield (r1 + r2 + r3)
result.onComplete {
case Success(x) => {
// the code won't come here
println(s"\nresult = $x")
}
case Failure(e) => {
// the code comes here because of the intentional exception
val finishTime = currentTime
val delta = finishTime - startTime
System.err.println(s"delta = $delta")
System.err.println("Failure happened!")
// just a short message; i don't care about the full exception
System.err.println(e.getMessage)
}
}
// important for a little parallel demo: keep the main
// thread of the jvm alive
sleep(4000)
def sleep(time: Long) = Thread.sleep(time)
def currentTime = System.currentTimeMillis()
}
When I run this App
, the Failure
case isn’t reached for 2008 ms, just a bit longer than the run time of the longest-running Future, f1
. Here’s the output:
delta = 2008
Failure happened!
Ka-boom!
This tells me that the exception in f2
causes the code to go to the Failure
case of onComplete
, but, it doesn’t get there until f1
finishes running. More specifically, the exception thrown by f2
doesn’t cause any part of the process to short-circuit after 550 ms. You can adjust the sleep times in the futures to demonstrate this for yourself.
I was just working on a Scala application to combine a few Futures and then work with the result using onComplete
, and needed to see the effect that exceptions have on the process. So I created this example, and thought I’d share the results here so it might help someone else.
Update: the order matters
I didn’t think to test this initially, but if I change the order of the Futures in the for-expression, putting f2
first:
val result = for { r2 <- f2 //the exception-throwing future, runs 550ms r1 <- f1 r3 <- f3 } yield (r1 + r2 + r3)
the output is different:
delta= 558 Failure happened! Ka-boom!
In this example, the for-expression short-circuits immediately after f2
throws its exception after 550 ms.
In the real world there may be no way for you to know which Future will return (or throw an exception) first, but this hard-coded example shows that the order is important. (Note that if you remove the exception from f2
, the code runs in just over 2000 ms, which confirms that the three futures run in parallel in the success case.)