A look at how exceptions work with Scala Futures and the onComplete ‘Failure’ case

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.)