1. Overview
In modern-day programming, applications need to support concurrency and parallelism. In this tutorial, we’ll take a look at asynchronous programming in Scala and how to handle the results synchronously.
2. Futures
Working with threads can become cumbersome, especially when writing large applications. To mitigate this problem, Scala provides the Future API to help simplify the code and provide functional asynchronous abstractions.
A Future acts as a placeholder object for a value that may not yet exist. It gives us a simple way to run a computation concurrently. A Future starts running concurrently upon creation and returns a result at some point in the future.
We can create a Future by calling the apply method on the Future object. Let’s say we want to retrieve the content from a URL. For the purpose of testing, let’s mock the response instead of actually invoking the URL:
def fetchDataFrom(url : String, waitTime : Long = 0L) : Future[String] = Future {
Thread.sleep(waitTime)
s"Mock response from $url"
}
fetchDataFrom("https://www.baeldung.com")
However, this will return an error from the compiler:
Cannot find an implicit ExecutionContext. You might pass an (implicit ec: ExecutionContext) parameter to your method
or import scala.concurrent.ExecutionContext.Implicits.global.
Future {
The compiler tells us that there is no execution context available. Futures are run on a separate thread, and the abstraction that provides the ability to run on a different thread is an ExecutionContext, which we didn’t provide.
To run this example successfully, we need to import scala.concurrent.ExecututionContext.Implicits.global.
3. Future Completion
Since a Future executes on a different thread, the thread that created the Future is free to continue execution. In our previous example, the program finished because there wasn’t any more work to do on the main thread, while our Future, which was executed on another thread, may not have completed.
The result of a Future is a Try, which represents the computation as either a success or a failure.
Now how do we extract the result from a Future? One way we could do this is to prevent the main thread from shutting down by adding a Thread.sleep call:
fetchDataFrom("https://www.baeldung.com").onSuccess(result => println(result))
Thread.sleep(1000)
Using Thread.sleep(), however, is not an ideal solution because we won’t always know what the response time will be.
Now, what if there was some way we could tell the main thread to wait until a Future completes and only continue execution after? This is where the Await API comes in.
3.1. Await.ready
The Await class contains a ready method that blocks the calling thread until the Future completes or times-out.
Let’s apply the ready method to our example:
val fut = fetchDataFrom("https://www.baeldung.com")
fut.isCompleted shouldBe false
val completedFuture: Future[String] = Await.ready(fut, 2.seconds)
fut shouldBe completedFuture
completedFuture.isCompleted shouldBe true
completedFuture.isInstanceOf[Future[String]] shouldBe true
val assertion = completedFuture.value match {
case Some(result) => result.isSuccess
case _ => false
}
assertion shouldBe true
Await.ready takes two parameters, the first being the Future we want to wait for, and the other being the maximum time duration. We may assume this works like Thread.sleep where only after the time supplied does the calling thread wake up. However, that isn’t the case with Await.ready.
The time duration supplied to Await.ready defines the maximum amount of time the calling thread is allowed to wait, and if the Future is not completed within that time frame, it throws a java.util.concurrent.TimeoutException.
Await.ready returns the original completed Future as either a Success or a Failure. It’s then up to the caller of the code to safely use or extract the value.
3.2. Await.result
Await.result is similar to Await.ready in that it blocks the calling thread until the Future completes or times-out. The major difference is that it tries to extract the value from the result of the Future rather than returning the completed Future.
Let’s see what happens when we apply the result method:
val fut = fetchDataFrom("https://www.baeldung.com")
fut.isCompleted shouldBe false
val completedFutureResult: String = Await.result(fut, 2.seconds)
completedFutureResult.isInstanceOf[String] shouldBe true
It’s important to note that the result is a String instead of a Future[String] as in the case of Await.ready.
Additionally, we can write Await.result in terms of Await.ready:
val result: String = Await.ready(fut, 5.seconds).value.get.get
We may think that using Await.result would be an easier alternative to using Await.ready, but the approach of directly extracting the value from a Future is not advisable as the Future could complete with an exception wrapped in a Failure.
If we attempt to extract this value when it is an exception, it will cause our program to crash.
4. Await.result vs. Await.ready
Both API’s block for at most the given duration. However, Await.result tries to return the Future result as soon as possible and throws an exception if the Future fails with an exception while Await.ready returns the completed Future from which the result (Success or Failure) can safely be extracted.
Let’s take a closer look at the differences:
def futureWithoutException(): Future[String] = Future {
"Hello"
}
def futureWithException(): Future[String] = Future {
throw new NullPointerException()
}
Here, we have two Future‘s who are expected to asynchronously return a String, the latter is bound to fail while the former is bound to succeed. Let’s see how our program responds to handling these Futures using Await.ready versus using Await.result:
val f1 = Await.ready(futureWithoutException, 2.seconds)
assert(f1.isInstanceOf[Future[String]] && f1.value.get.get.contains("Hello"))
val f2 = Await.ready(futureWithException, 2.seconds)
assert(f2.isInstanceOf[Future[String]] && f2.value.get.failed.get.isInstanceOf[NullPointerException])
val f3 = Await.result(futureWithoutException, 2.seconds)
assert(f3.isInstanceOf[String] && f3.contains("Hello"))
assert (intercept[Exception] { Await.result(futureWithException, 2.seconds)}.isInstanceOf[NullPointerException])
In the above example, we clearly see that using Await.result will result in the program throwing a NullPointerException as it tries to extract the value from what was a NullPointerException wrapped in a Failure.
Another thing to note with using Await.result is that it becomes tricky trying to figure out if the computation actually failed or just timed-out in code.
As a rule of thumb, it’s advisable to avoid using Await unless extremely necessary.
5. Conclusion
In this article, we’ve seen how Scala enables us to write asynchronous programs and various ways we could handle the result. We also discussed the difference between the methods of synchronously handling Futures as well as develop a good intuition into how they both handle Futures. As always, the source code can be found over on GitHub.