Chapter 9: A Beginner's Tutorial To Using Scala Futures
In this section, we will go over how to use Scala Futures to perform asynchronous non-blocking operations in parallel. You can find additional details on using Futures from the official Scala API documentation on Futures.
Throughout the tutorials we will use the popular IntelliJ IDEA which we've setup in Chapter 1. I hope that by now you are more comfortable using IntelliJ. If not, feel free to review the previous tutorials from Chapter 1!
In this chapter, we will provide tutorials on the topics below. If none of these make any sense right now, that's OK :) So let's get started!
Source Code:
- The source code is available on the allaboutscala GitHub repository.
Scala Futures:
- Introduction
- Method with future as return type
- Non blocking future result
- Chain futures using flatMap
- Chain futures using for comprehension
- Future option with for comprehension
- Future option with map
- Composing futures
- Future sequence
- Future traverse
- Future foldLeft
- Future reduceLeft
- Future firstCompletedOf
- Future zip
- Future zipWith
- Future andThen
- Future configure threadpool
- Future recover
- Future recoverWith
- Future fallbackTo
- Future promise
Introduction
This tutorial is an extension to the official Scala documentation on Scala Futures. We will provide short code snippets to help you get familiar with using Scala Futures to easily write asynchronous non-blocking operations.
Method with future as return type
1. Define a method which returns a Future
In Chapter 3 A Beginner's Tutorial To Using Function in Scala, we showed how to define and use methods and functions in Scala. The method below builds on the concepts we've learned from Chapter 3 and shows how to create a method which is intended to run asynchronously.
The method donutStock() will return an Int to represent the number of donuts we have in stock for a given type of donut. Note however that instead of returning an Int type, we are returning a Future of type Int, i.e., Future[Int].
println("Step 1: Define a method which returns a Future")
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
def donutStock(donut: String): Future[Int] = Future {
// assume some long running database operation
println("checking donut stock")
10
}
NOTE:
- We've had to import scala.concurrent.Future to have access to the Future type
- We've also had to import scala.concurrent.ExecutionContext.Implicits.global which will place a default thread pool in scope on which our Future will be executed asynchronously. If you are not familiar with ExecutionContext, that's OK, we will provide additional examples in this chapter.
2. Call method which returns a Future
Next, we call our donutStock() method from Step 1 and pass in vanilla donut as input parameter. The donutStock() method will run asynchronously and for the purpose of this example are using Await.result() to block our main program and wait for the result from the donutStock() method.
println("\nStep 2: Call method which returns a Future")
import scala.concurrent.Await
import scala.concurrent.duration._
val vanillaDonutStock = Await.result(donutStock("vanilla donut"), 5 seconds)
println(s"Stock of vanilla donut = $vanillaDonutStock")
You should see the following output when you run your Scala application in IntelliJ:
Step 2: Call method which returns a Future
checking donut stock
Stock of vanilla donut = 10
NOTE:
- In general, avoid blocking!
- As we progress through the examples in this tutorial, we will show other solutions to avoid blocking on futures.
Non blocking future result
1. Define a method which returns a Future
In the Method with future return type section, we showed how to create an asynchronous method by adding the Future return type. Let's reuse the method donutStock() which returns a Future of type Int to represent the donut stock for a particular donut.
println("Step 1: Define a method which returns a Future")
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
def donutStock(donut: String): Future[Int] = Future {
// assume some long running database operation
println("checking donut stock")
10
}
2. Non blocking future result
Instead of blocking our main program using Await.result(), we will make use of Future.onComplete() callback to capture the result of a Future.
println("\nStep 2: Non blocking future result")
import scala.util.{Failure, Success}
donutStock("vanilla donut").onComplete {
case Success(stock) => println(s"Stock for vanilla donut = $stock")
case Failure(e) => println(s"Failed to find vanilla donut stock, exception = $e")
}
Thread.sleep(3000)
You should see the following output when you run your Scala application in IntelliJ:
Step 2: Non blocking future result
checking donut stock
Stock for vanilla donut = 10
NOTE:
- With Future.onComplete() we are no longer blocking for the result from the Future but instead we will receive a callback for either a Success or a Failure.
- As such, we've also had to import scala.util.{Failure, Success}
- Surely though the Thread.sleep() is blocking our main thread so that we can see the asynchronous result from the future. In a real application, you will most certainly not use Thread.sleep() but instead "react" to the result returned by the future.
Chain futures using flatMap
In this section, we will show how you can easily chain futures by using the flatMap() method.
1. Define a method which returns a Future
Similar to the previous examples, we'll define a method called donutStock() which returns a Future[Int].
println("Step 1: Define a method which returns a Future")
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
def donutStock(donut: String): Future[Int] = Future {
// assume some long running database operation
println("checking donut stock")
10
}
2. Define another method which returns a Future
Next, we define another method called buyDonuts() and it also returns a future but of type Boolean. Our assumption is that the donut stock quantity returned from Step 1 should be passed-through as input parameter to the buyDonuts() method.
println("\nStep 2: Define another method which returns a Future")
def buyDonuts(quantity: Int): Future[Boolean] = Future {
println(s"buying $quantity donuts")
true
}
3. Chaining Futures using flatMap
To sequence multiple futures in order, you can make use of the flatMap() method. In our example, we will chain the two future operations donutStock() and buyDonuts() by using flatMap() method as shown below.
println("\nStep 3: Chaining Futures using flatMap")
val buyingDonuts: Future[Boolean] = donutStock("plain donut").flatMap(qty => buyDonuts(qty))
import scala.concurrent.Await
import scala.concurrent.duration._
val isSuccess = Await.result(buyingDonuts, 5 seconds)
println(s"Buying vanilla donut was successful = $isSuccess")
You should see the following output when you run your Scala application in IntelliJ:
Step 3: Chaining Futures using flatMap
checking donut stock
buying 10 donuts
Buying vanilla donut was successful = true
Chain futures using for comprehension
In the previous example, we showed how you can make use of flatMap() method to chain multiple futures. Scala provides a syntactic sugar for flatMap() method which is called the for comprehension. To this end, let's re-write the flatMap() example above and use for comprehension to chain and sequence futures.
1. Define a method which returns a Future
Let's define our donutStock() method which returns a Future of type Int.
println("Step 1: Define a method which returns a Future")
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
def donutStock(donut: String): Future[Int] = Future {
// assume some long running database operation
println("checking donut stock")
10
}
2. Define another method which returns a Future
Similar to our previous examples, we'll define another method called buyDonuts() which returns a Future of type Boolean.
println("\nStep 2: Define another method which returns a Future")
def buyDonuts(quantity: Int): Future[Boolean] = Future {
println(s"buying $quantity donuts")
true
}
3. Chaining Futures using for comprehension
To chain and sequence the donutStock() and buyDonuts() methods, you can easily make use of the for comprehension syntax as shown below.
println("\nStep 3: Chaining Futures using for comprehension")
for {
stock <- donutStock("vanilla donut")
isSuccess <- buyDonuts(stock)
} yield println(s"Buying vanilla donut was successful = $isSuccess")
Thread.sleep(3000)
You should see the following output when you run your Scala application in IntelliJ:
Step 3: Chaining Futures using for comprehension
checking donut stock
buying 10 donuts
Buying vanilla donut was successful = true
Future option with for comprehension
Sometimes your futures may return Option of some given type. When we get to the functional aspects of Scala, we will show a more elegant way to deal with Future Option using monad transformers. For now, we will continue to use the for comprehension.
1. Define a method which returns a Future Option
The donutStock() method below will return a Future Option of type Int, i.e. Future[Option[Int]] instead of simply returning an Int.
println("Step 1: Define a method which returns a Future Option")
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
def donutStock(donut: String): Future[Option[Int]] = Future {
// assume some long running database operation
println("checking donut stock")
if(donut == "vanilla donut") Some(10) else None
}
2. Define another method which returns a Future
In this step, we will re-use our familiar buyDonuts() method and it will return a Future of type Boolean.
println("\nStep 2: Define another method which returns a Future")
def buyDonuts(quantity: Int): Future[Boolean] = Future {
println(s"buying $quantity donuts")
if(quantity > 0) true else false
}
3. Chaining Future Option using for comprehension
Since donutStock() method returns a Future Option of type Int, someStock is in fact an Option of type Int. In order to pass-through someStock to the buyDonuts() method as input parameter, we are making use of the getOrElse() method from Option.
println("\nStep 3: Chaining Future Option using for comprehension")
for {
someStock <- donutStock("vanilla donut")
isSuccess <- buyDonuts(someStock.getOrElse(0))
} yield println(s"Buying vanilla donut was successful = $isSuccess")
You should see the following output when you run your Scala application in IntelliJ:
Step 3: Chaining Future Option using for comprehension
checking donut stock
buying 10 donuts
Buying vanilla donut was successful = true
Future option with map
In this example, we will show how you can access the value wrapped inside a Future of Option using the map function. Note however that using map is useful when you are most certainly working with a single Future as opposed to multiple Futures which require chaining.
1. Define a method which returns a Future Option
Let's make use of our donutStock() method which returns a Future Option of type Int.
println("Step 1: Define a method which returns a Future Option")
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
def donutStock(donut: String): Future[Option[Int]] = Future {
// assume some long running database operation
println("checking donut stock")
if(donut == "vanilla donut") Some(10) else None
}
2. Access value returned by future using map() method
By calling the map() method on the donutStock() future method, you can access the value returned by the future which in this case is an Option of type Int.
println(s"\nStep 2: Access value returned by future using map() method")
donutStock("vanilla donut")
.map(someQty => println(s"Buying ${someQty.getOrElse(0)} vanilla donuts"))
You should see the following output when you run your Scala application in IntelliJ:
Step 2: Access value returned by future using map() method
checking donut stock
Buying 10 vanilla donuts
Composing futures
We've in fact already shown in chain futures using flatMap and chain futures using for comprehension how to use the flatMap() function or the syntactic sugar of the for comprehension to sequence futures. In this section, we will provide some additional details on the idea of composing futures.
1. Define a method which returns a Future Option
Let's begin by defining our already familiar donutStock() method which returns a Future of type Int.
println("Step 1: Define a method which returns a Future Option")
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
def donutStock(donut: String): Future[Option[Int]] = Future {
// assume some long running database operation
println("checking donut stock")
if(donut == "vanilla donut") Some(10) else None
}
2. Define another method which returns a Future
Next, we'll define the buyDonut() method which returns a Future of type Boolean.
println("\nStep 2: Define another method which returns a Future")
def buyDonuts(quantity: Int): Future[Boolean] = Future {
println(s"buying $quantity donuts")
if(quantity > 0) true else false
}
3. Calling map() method over multiple futures
You could certainly use the map() function to chain the donutStock() future method with the buyDonuts() future.
Note however that using map() creates nesting in the return type. In the example below, the return type of resultFromMap() is a Future of Future of type Boolean. If you had to work on the restulFromMap() type, then you would be stuck in having to unwrap the nesting in order to access the values within the futures. In future tutorials, we will show how to make use of monadic transformers to help you work with nesting.
println(s"\nStep 3: Calling map() method over multiple futures")
val resultFromMap: Future[Future[Boolean]] = donutStock("vanilla donut")
.map(someQty => buyDonuts(someQty.getOrElse(0)))
Thread.sleep(1000)
You should see the following output when you run your Scala application in IntelliJ:
Step 3: Calling map() method over multiple futures
checking donut stock
buying 10 donuts
4. Calling flatMap() method over multiple future
The example below is from the previous section where we showed how to sequence futures using flatMap(). It is worth comparing the return type from using flatMap() instead of map(). With flatMap(), there is no nesting and the return type for restulFromFlatMap below is a Future of type Boolean.
println(s"\nStep 4: Calling flatMap() method over multiple futures")
val resultFromFlatMap: Future[Boolean] = donutStock("vanilla donut")
.flatMap(someQty => buyDonuts(someQty.getOrElse(0)))
Thread.sleep(1000)
You should see the following output when you run your Scala application in IntelliJ:
Step 4: Calling flatMap() method over multiple futures
checking donut stock
buying 10 donuts
Future sequence
In this section, we will show how to fire a bunch of future operations and wait for their results by using the Future.sequence() function. As noted in the Scala API documentation, the sequence function is useful when you have to reduce a number of futures into a single future. Moreover, these futures will be non-blocking and run in parallel which also imply that the order of the futures is not guaranteed as they can be interleaved.
1. Define a method which returns a Future Option of Int
Similar to our previous example, we'll begin with the donutStock() method which returns a Future Option of type Int. Note that we've also added a Thread.sleep() inside the method block to simulate a long running operation.
println("Step 1: Define a method which returns a Future Option of Int")
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
def donutStock(donut: String): Future[Option[Int]] = Future {
println("checking donut stock ... sleep for 2 seconds")
Thread.sleep(2000)
if(donut == "vanilla donut") Some(10) else None
}
2. Define another method which returns a Future[Boolean]
In Step 2, we create the buyDonuts() method which returns a Future of type Boolean. Similar to Step 1, we've added a Thread.sleep() inside the method block.
println("\nStep 2: Define another method which returns a Future[Boolean]")
def buyDonuts(quantity: Int): Future[Boolean] = Future {
println(s"buying $quantity donuts ... sleep for 3 seconds")
Thread.sleep(3000)
if(quantity > 0) true else false
}
3. Define another method for processing payments and returns a Future[Unit]
To add a bit more complexity compared to the previous future code snippets, we create another method name processPayment() which returns a Future of type Unit. Once again, we are using a Thread.sleep() inside the method block.
println("\nStep 3: Define another method for processing payments and returns a Future[Unit]")
def processPayment(): Future[Unit] = Future {
println("processPayment ... sleep for 1 second")
Thread.sleep(1000)
}
4. Combine future operations into a List
For Step 4, we combine the futures from Step 1, 2 and 3 into a Immutable List. Note also that the return type of the resulting future inside the List is of type Any.
println("\nStep 4: Combine future operations into a List")
val futureOperations: List[Future[Any]] = List(donutStock("vanilla donut"), buyDonuts(10), processPayment())
5. Call Future.sequence to run the future operations in parallel
Finally we can call Future.sequence and pass-through the List of futures from Step 4 in order to run our future operations in parallel. As a reminder, you will notice that in the output produced, the operations are interleaved.
println(s"\nStep 5: Call Future.sequence to run the future operations in parallel")
val futureSequenceResults = Future.sequence(futureOperations)
futureSequenceResults.onComplete {
case Success(results) => println(s"Results $results")
case Failure(e) => println(s"Error processing future operations, error = ${e.getMessage}")
}
You should see the following output when you run your Scala application in IntelliJ:
Step 4: Combine future operations into a List
checking donut stock ... sleep for 2 seconds
processPayment ... sleep for 1 second
buying 10 donuts ... sleep for 3 seconds
Future traverse
The Future.traverse() function is fairly similar to the Future.sequence() function. As per the Scala API documentation, the traverse function also allows you to fire a bunch of future operations in parallel and wait for their results. The traverse function, though, has the added benefit of allowing you to apply a function over the future operations.
1. Define a method which returns a Future Option
To start with, we'll define our familiar donutStock() method and have it return a Future Option of type Int. For input parameter of vanilla donut, we'll expect Some(10) to be returned, and for all other inputs, the result will be a None.
println("Step 1: Define a method which returns a Future Option")
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
def donutStock(donut: String): Future[Option[Int]] = Future {
println("checking donut stock")
if(donut == "vanilla donut") Some(10) else None
}
2. Create a List of future operations
In Step 2, we will create an Immutable List of future operations for the donutStock() method.
println(s"\nStep 2: Create a List of future operations")
val futureOperations = List(
donutStock("vanilla donut"),
donutStock("plain donut"),
donutStock("chocolate donut")
)
3. Call Future.traverse to convert all Option of Int into Int
A reminder that the futureOperations List from Step 2 will have a mix of Some(10) and None values depending on the input parameter that was passed-through to the donutStock() method. Using the Future.traverse function, we can easily convert all the Option[Int] into just Int type.
println(s"\nStep 3: Call Future.traverse to convert all Option of Int into Int")
val futureTraverseResult = Future.traverse(futureOperations){ futureSomeQty =>
futureSomeQty.map(someQty => someQty.getOrElse(0))
}
futureTraverseResult.onComplete {
case Success(results) => println(s"Results $results")
case Failure(e) => println(s"Error processing future operations, error = ${e.getMessage}")
}
You should see the following output when you run your Scala application in IntelliJ:
Step 3: Call Future.traverse to convert all Option of Int into Int
Results List(10, 0, 0)
NOTE:
- The return type of futureTraverseResult is Future[List[Int]] instead of Future[List[Option[Int]]]
- The return type of results is List[Int] instead of List[Option[Int]]
Future foldLeft
In chapter 8 on Collection Functions, we introduced the foldLeft function. Similarly, Scala provides a foldLeft function for futures and, as per the Scala API documentation, the foldLeft on your future operations will be run asynchronously from left to right. As per the write up of this code snippet, the Scala version being used is 12.2.4 and note that Future.fold() is now deprecated in favour of Future.foldLeft().
1. Define a method which returns a Future Option
Let's start with our familiar donutStock() method which return a Future of Option of type Int.
println("Step 1: Define a method which returns a Future Option")
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
def donutStock(donut: String): Future[Option[Int]] = Future {
println("checking donut stock")
if(donut == "vanilla donut") Some(10) else None
}
2. Create a List of future operations
For Step 2, we will create an Immutable List of donutStock() futures.
println(s"\nStep 2: Create a List of future operations")
val futureOperations = List(
donutStock("vanilla donut"),
donutStock("plain donut"),
donutStock("chocolate donut"),
donutStock("vanilla donut")
)
3: Call Future.foldLeft to fold over futures results from left to right
Using Future.foldLeft, you can easily aggregate the results returned by the future operations. Since donutStock() method returns an Option of Int, inside the foldLeft, we will use someQty.getOrElse(0) to either pass a valid quantity or a default value of zero to the accumulator.
println(s"\nStep 3: Call Future.foldLeft to fold over futures results from left to right")
val futureFoldLeft = Future.foldLeft(futureOperations)(0){ case (acc, someQty) =>
acc + someQty.getOrElse(0)
}
futureFoldLeft.onComplete {
case Success(results) => println(s"Results $results")
case Failure(e) => println(s"Error processing future operations, error = ${e.getMessage}")
}
You should see the following output when you run your Scala application in IntelliJ:
Step 3: Call Future.foldLeft to fold over futures results from left to right
Results 20
Future reduceLeft
In the previous section, we've introduced Future.foldLeft() function to allow you to operate on the results from future operations in a left to right manner. Scala also provides a Future.reduceLeft() function which has a similar behaviour. Unlike foldLeft(), however, reduceLeft() does not allow you to provide a default value.
1. Define a method which returns a Future Option
As usual, let's define our donutStock() method which returns a Future Option of type Int.
println("Step 1: Define a method which returns a Future Option")
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
def donutStock(donut: String): Future[Option[Int]] = Future {
println("checking donut stock")
if(donut == "vanilla donut") Some(10) else None
}
2. Create a List of future operations
Next, we'll create a bunch of donutStock() future operations and store them inside an Immutable List.
println(s"\nStep 2: Create a List of future operations")
val futureOperations = List(
donutStock("vanilla donut"),
donutStock("plain donut"),
donutStock("chocolate donut"),
donutStock("vanilla donut")
)
3. Call Future.reduceLeft to fold over futures results from left to right
As a reminder, in the Future.foldLeft() code snippet, we had the opportunity to provide a default value of zero and hence our accumulator type was an Int. With Future.reduceLeft(), we cannot provide a default value and hence the accumulator is of type Option[Int]. Likewise, the return type of results is in the onComplete() closure is Option[Int] instead of Int.
println(s"\nStep 3: Call Future.reduceLeft to fold over futures results from left to right")
val futureFoldLeft = Future.reduceLeft(futureOperations){ case (acc, someQty) =>
acc.map(qty => qty + someQty.getOrElse(0))
}
futureFoldLeft.onComplete {
case Success(results) => println(s"Results $results")
case Failure(e) => println(s"Error processing future operations, error = ${e.getMessage}")
}
You should see the following output when you run your Scala application in IntelliJ:
Step 3: Call Future.reduceLeft to fold over futures results from left to right
Results Some(20)
Future firstCompletedOf
There may be times when you'd be looking to fire a bunch of futures and continue processing as soon as you've received the first result from any one of them. This behaviour is perhaps observed in micro-services architecture for sharding traffic. With this in mind, Scala provides a handy Future.firstCompletedOf() function to achieve just that. As per the Scala API documentation, firstCompletedOf() function, as its name implies, will return the first future that completes.
1. Define a method which returns a Future Option
We'll once more reuse our familiar donutStock() method which returns a Future Option of type Int.
println("Step 1: Define a method which returns a Future Option")
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
def donutStock(donut: String): Future[Option[Int]] = Future {
println("checking donut stock")
if(donut == "vanilla donut") Some(10) else None
}
2. Create a List of future operations
Next, we'll create an Immutable List of donutStock() operations as follows:
println(s"\nStep 2: Create a List of future operations")
val futureOperations = List(
donutStock("vanilla donut"),
donutStock("plain donut"),
donutStock("chocolate donut"),
donutStock("vanilla donut")
)
3. Call Future.firstCompletedOf to get the results of the first future that completes
Finally, we call Future.firstCompletedOf() and pass-through the list of donutStock() future operations. Note, however, in the sample run shown below, the results returned is a None. A reminder that firstCompletedOf() is non-deterministic and it will return any one of the futures which finish first. You can easily simulate this in our simple example by keep replaying the application. You should notice that in some instances the results returned may be Some(10).
println(s"\nStep 3: Call Future.firstCompletedOf to get the results of the first future that completes")
val futureFirstCompletedResult = Future.firstCompletedOf(futureOperations)
futureFirstCompletedResult.onComplete {
case Success(results) => println(s"Results $results")
case Failure(e) => println(s"Error processing future operations, error = ${e.getMessage}")
}
You should see the following output when you run your Scala application in IntelliJ:
Step 3: Call Future.firstCompletedOf to get the results of the first future that completes
checking donut stock
checking donut stock
Results None
Future zip
In this section, we will demonstrate how you can use Future.zip to combine the results of two future operations into a single tuple. As per the Scala API documentation, the Future.zip will create a new future whose return type will be a tuple holding the return types of the two futures.
1. Define a method which returns a Future Option
Let's start with our familiar donutStock() method which returns a Future Option of type Int.
println("Step 1: Define a method which returns a Future Option")
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
def donutStock(donut: String): Future[Option[Int]] = Future {
println("checking donut stock")
if(donut == "vanilla donut") Some(10) else None
}
2. Define a method which returns a Future Double for donut price
For the purpose of this example, let's create another method donutPrice(). It will return a Future of type double to represent the price of a donut.
println(s"\nStep 2: Define a method which returns a Future Double for donut price")
def donutPrice(): Future[Double] = Future.successful(3.25)
3. Zip the values of the first future with the second future
To combine the results of the two future operations, donutStock() and donutPrice(), into a single tuple, you can make use of the zip method as shown below. As a result, note that the return type of donutStockAndPriceOperation is Future[(Option[Int], Double)]. Option[Int] is the return type from donutStock(), Double is the return type from donutPrice(), and both types are enclosed inside a tuple.
println(s"\nStep 3: Zip the values of the first future with the second future")
val donutStockAndPriceOperation = donutStock("vanilla donut") zip donutPrice()
donutStockAndPriceOperation.onComplete {
case Success(results) => println(s"Results $results")
case Failure(e) => println(s"Error processing future operations, error = ${e.getMessage}")
}
You should see the following output when you run your Scala application in IntelliJ:
Step 3: Zip the values of the first future with the second future
checking donut stock
Results (Some(10),3.25)
Future zipWith
This section will be an extension to the previous Future zip tutorial. Similar to future zip() method, Scala also provides a handy future zipWith() method. In addition to combining the results of two futures, the zipWith() method allows you to pass-through a function which can be applied to the results.
1. Define a method which returns a Future Option
Let's start with our donutStock() method which returns a Future Option of type Int.
println("Step 1: Define a method which returns a Future Option")
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
def donutStock(donut: String): Future[Option[Int]] = Future {
println("checking donut stock")
if(donut == "vanilla donut") Some(10) else None
}
2. Define a method which returns a Future Double for donut price
Next, let's create another method named donutPrice() which returns a Future of type Double representing the price of a particular donut.
println(s"\nStep 2: Define a method which returns a Future Double for donut price")
def donutPrice(): Future[Double] = Future.successful(3.25)
3. Define a value function to convert Tuple (Option[Int], Double) to Tuple (Int, Double)
Similar to the last tutorial on future zip, when we zip donutStock() Future with donutPrice() future, the return type of the result was a tuple of Option[Int] and Double. Option[Int] type represents the quantity from method donutStock() and Double type represents the price from donutPrice(). The qtyAndPriceF function below is a dummy example to map the Int value from an Option and we will pass this function to future zipWith().
println(s"\nStep 3: Define a value function to convert Tuple (Option[Int], Double) to Tuple (Int, Double)")
val qtyAndPriceF: (Option[Int], Double) => (Int, Double) = (someQty, price) => (someQty.getOrElse(0), price)
4. Call Future.zipWith and pass-through function qtyAndPriceF
By passing-through the function qtyAndPriceF from Step 3 to the zipWith() method, the type (Option[Int], Double) will be transformed into (Int, Double).
println(s"\nStep 4: Call Future.zipWith and pass-through function qtyAndPriceF")
val donutAndPriceOperation = donutStock("vanilla donut").zipWith(donutPrice())(qtyAndPriceF)
donutAndPriceOperation.onComplete {
case Success(result) => println(s"Result $result")
case Failure(e) => println(s"Error processing future operations, error = ${e.getMessage}")
}
You should see the following output when you run your Scala application in IntelliJ:
Step 4: Call Future.zipWith and pass-through function qtyAndPriceF
checking donut stock
Result (10,3.25)
Future andThen
In this section, we will introduce the future andThen() function. As per the Scala API documentation, andThen() is used whenever you have a need to apply a side-effect function on the result returned by the future.
1. Define a method which returns a Future
Similar to our previous examples, let's start by creating our donutStock() method which returns a Future of type Int.
println("Step 1: Define a method which returns a Future")
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
def donutStock(donut: String): Future[Int] = Future {
// assume some long running database operation
println("checking donut stock")
10
}
2. Call Future.andThen with a PartialFunction
Next, we will pass-through a Partial Function to the future returned by the donutStock() method. Within this partial function, we get access to the donut stock quantity (the Int type returned by the donutStock() future).
Note however, that the stockQty within the andThen() function is not of type Int but instead a Try[Int]. If you were to look at the Scala implementation of andThen() function, you will notice that a try-catch is used which results in a Try[T]. For the purpose of this illustration, we simply print the stockQty and you can see that the value of stockQty is Some(10).
println(s"\nStep 2: Call Future.andThen with a PartialFunction")
val donutStockOperation = donutStock("vanilla donut")
donutStockOperation.andThen { case stockQty => println(s"Donut stock qty = $stockQty")}
You should see the following output when you run your Scala application in IntelliJ:
Step 2: Call Future.andThen with a PartialFunction
checking donut stock
Donut stock qty = Success(10)
Future configure threadpool
So far, in our previous code snippets using future, we've introduced the Scala global ExecutionContext: scala.concurrent.ExecutionContext.Implicits.global. There may be situations where you have a need to control your threading behaviour. In this tutorial, we will show how you can provide your own ExecutionContext.
1. Define an ExecutionContext
To start with, let's make use of java.util.concurrent.Executors class which will provide us with an Executor. For the purpose of this example, we are using a single thread pool executor (Executors.newSingleThreadExecutor), but could use any other executors such as Executors.newCachedThreadPool or Executors.newFixedThreadPool.
In order to place our executor in scope, we pass it through to the ExecutionContext.fromExecutor() method.
println("Step 1: Define an ExecutionContext")
val executor = Executors.newSingleThreadExecutor()
implicit val ec = scala.concurrent.ExecutionContext.fromExecutor(executor)
2. Define a method which returns a Future
Next, we define our usual donutStock() method which returns a Future of type Int.
println("\nStep 2: Define a method which returns a Future")
import scala.concurrent.Future
def donutStock(donut: String): Future[Int] = Future {
// assume some long running database operation
println("checking donut stock")
10
}
3. Call method which returns a Future
Similar to our previous examples, we call the donutStock() method and we register the onComplete() callback to access the result of our future operation. Since we've explicitly created an executor, it's perhaps also a good idea to shut it down - this obviously depends on the behaviour and workflow of your application.
println("\nStep 3: Call method which returns a Future")
val donutStockOperation = donutStock("vanilla donut")
donutStockOperation.onComplete {
case Success(donutStock) => println(s"Results $donutStock")
case Failure(e) => println(s"Error processing future operations, error = ${e.getMessage}")
}
Thread.sleep(3000)
executor.shutdownNow()
Future recover
In this section, we will show how to use the future recover() function to help you work around exceptions that may arise from a future operation. Typically, though, you would want to recover from known exceptions that your future may throw as opposed to trapping any random exception.
1. Define a method which returns a Future
As usual, let's start by creating our donutStock() method which returns a Future of type Int. Within the body of our donutStock() method, we will throw an IllegalStateException for any donut which does not match the String vanilla donut.
println("Step 1: Define a method which returns a Future")
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
def donutStock(donut: String): Future[Int] = Future {
if(donut == "vanilla donut") 10
else throw new IllegalStateException("Out of stock")
}
2. Execute donutStock() future operation
In this step, we will call the donutStock() method and pass-through the String vanilla donut as input. We would expect this future to complete just fine since we only throw an exception if the input String is not vanilla donut.
println("\nStep 2: Execute donutStock() future operation")
donutStock("vanilla donut")
.onComplete {
case Success(donutStock) => println(s"Results $donutStock")
case Failure(e) => println(s"Error processing future operations, error = ${e.getMessage}")
}
You should see the following output when you run your Scala application in IntelliJ:
Step 2: Execute donutStock() future operation
Results 10
3. Call Future.recover to recover from a known exception
In this step, however, we will pass-through the input String of unknown donut as parameter to the donutStock() method. As a result, we know based on the implementation of the donutStock() method that this input String will throw an exception. To this end, we can use the recover() function to help us continue the flow of our program. In our example below, we simply return an Int value of 0 donut stock.
Note also that we were explicitly recovering for only IllegalStateException. For a more general case, you could catch NonFatal exception using: case NonFatal(e) => 0
println("\nStep 3: Call Future.recover to recover from a known exception")
donutStock("unknown donut")
.recover { case e: IllegalStateException if e.getMessage == "Out of stock" => 0 }
.onComplete {
case Success(donutStock) => println(s"Results $donutStock")
case Failure(e) => println(s"Error processing future operations, error = ${e.getMessage}")
}
You should see the following output when you run your Scala application in IntelliJ:
Step 3: Call Future.recover to recover from a known exception
Results 0
Future recoverWith
In the previous section, we introduced future recover() method. Similarly, Scala provides a future recoverWith() method but it requires a return type of Future. You can visually notice the difference between recover and recoverWith by comparing their method signatures as per the official Scala API documentation.
recover:
def recover[U >: T](pf: PartialFunction[Throwable, U])
recoverWith:
def recoverWith[U >: T](pf: PartialFunction[Throwable, Future[U]])
1. Define a method which returns a Future
Let's get started by creating our familiar donutStock() method which returns a Future of type Int. Note that we will throw an IllegalStateException() for any inputs that are not vanilla donut.
println("Step 1: Define a method which returns a Future")
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
def donutStock(donut: String): Future[Int] = Future {
if(donut == "vanilla donut") 10
else throw new IllegalStateException("Out of stock")
}
2. Execute donutStock() future operation
In this step, we will pass-through the input parameter of vanilla donut and we should expect the future onComplete() callback to run successfully.
println("\nStep 2: Execute donutStock() future operation")
donutStock("vanilla donut")
.onComplete {
case Success(donutStock) => println(s"Results $donutStock")
case Failure(e) => println(s"Error processing future operations, error = ${e.getMessage}")
}
You should see the following output when you run your Scala application in IntelliJ:
Step 2: Execute donutStock() future operation
Results 10
3. Call Future.recoverWith to recover from a known exception
In the code snippet below, we will pass-through an input of unknown donut to the donutStock() method and as such should expect an exception to be thrown. By using the recoverWith() method, we can continue the execution flow of our program. As always, try to avoid throwing exceptions around in the first place! But there may be times when say dealing with I/O that it makes sense to recover from some known exceptions.
println("\nStep 3: Call Future.recoverWith to recover from a known exception")
donutStock("unknown donut")
.recoverWith { case e: IllegalStateException if e.getMessage == "Out of stock" => Future.successful(0) }
.onComplete {
case Success(donutStock) => println(s"Results $donutStock")
case Failure(e) => println(s"Error processing future operations, error = ${e.getMessage}")
}
You should see the following output when you run your Scala application in IntelliJ:
Step 3: Call Future.recoverWith to recover from a known exception
Results 0
Future fallbackTo
In the previous examples, we've showed how we can use the future recover() and recoverWith() methods to deal with known or expected exceptions. As mentioned in the official Scala API documentation on futures, Scala also provides a fallbackTo() method which allows you to provide an alternative method that can be called in the event of an exception.
1. Define a method which returns a Future
As usual, we start by defining our donutStock() method which returns a Future of type Int. We will also throw an IllegalStateException for any inputs other than vanilla donut.
println("Step 1: Define a method which returns a Future")
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
def donutStock(donut: String): Future[Int] = Future {
if(donut == "vanilla donut") 10
else throw new IllegalStateException("Out of stock")
}
2. Define another method which returns a Future to match a similar donut stock
Next, we'll create another method named similarDonutStock() which returns a Future of type Int. Let's not focus on the actual implementation of this method. Instead, we will show that we can use it in Step 3 below as input to the future fallbackTo() method.
println("\nStep 2: Define another method which returns a Future to match a similar donut stock")
def similarDonutStock(donut: String): Future[Int] = Future {
println(s"replacing donut stock from a similar donut = $donut")
if(donut == "vanilla donut") 20 else 5
}
3. Call Future.fallbackTo
We first call donutStock() method by passing through the input of plain donut. This will result in an IllegalStateException being thrown as per the implementation of the donutStock() method.
However, by registering the similarDonutStock() method as per the fallbackTo() method, the IllegalStateException will trigger the similarDonutStock() method to be called. This type of behaviour allows you to plan ahead and perhaps, in a real-life scenario, call a different donut stock service.
println("\nStep 3: Call Future.fallbackTo")
val donutStockOperation = donutStock("plain donut")
.fallbackTo(similarDonutStock("vanilla donut"))
.onComplete {
case Success(donutStock) => println(s"Results $donutStock")
case Failure(e) => println(s"Error processing future operations, error = ${e.getMessage}")
}
You should see the following output when you run your Scala application in IntelliJ:
Step 3: Call Future.fallbackTo
replacing donut stock from a similar donut = vanilla donut
Results 20
Future promise
So far we've been using future's return type to mark our methods to run asynchronously. Scala provides an abstraction over future and it is called a Promise. Promises can be useful when you want to capture the intent of your operation versus its behaviour.
1. Define a method which returns a Future
As usual, we start with our donutStock() method. Unlike the previous examples, though, let's keep the return type to be just an Int instead of Future[Int].
println("Step 1: Define a method which returns a Future")
import scala.concurrent.ExecutionContext.Implicits.global
def donutStock(donut: String): Int = {
if(donut == "vanilla donut") 10
else throw new IllegalStateException("Out of stock")
}
2. Define a Promise of type Int
Next, we create a Promise of type Int to capture the intent that when we call the method donutStock(), we should expect the result to be a stock quantity of type Int.
println(s"\nStep 2: Define a Promise of type Int")
val donutStockPromise = Promise[Int]()
3. Define a future from Promise
Using the donutStockPromise from Step 2, we can call the Promise.future method to get back a Future of type Int.
println("\nStep 3: Define a future from Promise")
val donutStockFuture = donutStockPromise.future
donutStockFuture.onComplete {
case Success(stock) => println(s"Stock for vanilla donut = $stock")
case Failure(e) => println(s"Failed to find vanilla donut stock, exception = $e")
}
4. Use Promise.success or Promise.failure to control execution of your future
We know that the donutStock() method will successfully return a value of type Int for the input parameter vanilla donut. In that case, we can call the Promise.success() and pass-through the donutStock() method.
On the other hand, calling donutStock() with a parameter other than vanilla donut will throw an IllegalStateException. In that case, we can call Promise.failure(). The example is somehow silly but it shows how we complete the Promise by either calling Promise.success() or Promise.failure()
println("\nStep 4: Use Promise.success or Promise.failure to control execution of your future")
val donut = "vanilla donut"
if(donut == "vanilla donut") {
donutStockPromise.success(donutStock(donut))
} else {
donutStockPromise.failure(Try(donutStock(donut)).failed.get)
}
You should see the following output when you run your Scala application in IntelliJ:
Step 3: Define a future from Promise
Stock for vanilla donut = 10
5. Completing Promise using Promise.complete() method
Instead of completing a promise using Promise.success() or Promise.failure(), you can use the Promise.complete() method. Similar to Step 4, we've registered the Future.onComplete() method to get back the result produced when the donutStockFuture2 is executed. In this case, we are expecting the result to be the IllegalStateException because our input to donutStock() method is unknown donut.
println("\nStep 5: Completing Promise using Promise.complete() method")
val donutStockPromise2 = Promise[Int]()
val donutStockFuture2 = donutStockPromise2.future
donutStockFuture2.onComplete {
case Success(stock) => println(s"Stock for vanilla donut = $stock")
case Failure(e) => println(s"Failed to find vanilla donut stock, exception = $e")
}
donutStockPromise2.complete(Try(donutStock("unknown donut")))
You should see the following output when you run your Scala application in IntelliJ:
Step 5: Completing Promise using Promise.complete() method
Failed to find vanilla donut stock, exception = java.lang.IllegalStateException: Out of stock