Scala - Futures



Futures are used to handle asynchronous computations in Scala. You can perform many operations concurrently in an efficient and non-blocking manner. You can write code that runs in parallel. It improves performance and responsiveness of your applications.

Future in Scala

Future represents a value that may not yet exist but will be available at some point in the future. Futures are placeholders for results of asynchronous computations. These can either succeed or fail. When the computation completes, the future is either completed successfully with a value or failed with an exception.

Futures are non-blocking and utilize callbacks and combinators for asynchronous programming. Following is the example which shows you how to create a future -

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

val future: Future[String] = Future {
  "Hello, World!"
}

future.foreach(result => println(result))

Save the above program in Demo.scala. The following commands are used to compile and execute this program:

Command

\>scalac Demo.scala
\>scala Demo

Output

Hello, World!

Creating Futures

To create a future, you need to use the Future object and pass a block of code to it. The code inside the block runs asynchronously. Following is the example which shows you how to create Future in Scala -

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

def longRunningComputation(): Int = {
  Thread.sleep(3000)
  42
}

val futureResult: Future[Int] = Future {
  longRunningComputation()
}

Execution Context

Futures require an ExecutionContext to run. The ExecutionContext manages the threads on which the futures execute. You can use the global execution context and create your own -

import scala.concurrent.ExecutionContext
import java.util.concurrent.Executors

val executorService = Executors.newFixedThreadPool(4)
implicit val customExecutionContext: ExecutionContext = 
   ExecutionContext.fromExecutorService(executorService)

Handling Results

You can use callbacks like onComplete, foreach, and map. You can handle the result of the future. You can define what should happen when the future completes using these callbacks -

import scala.util.{Success, Failure}

futureResult.onComplete {
  case Success(value) => println(s"Result: $value")
  case Failure(exception) => println(s"Error: ${exception.getMessage}")
}

The foreach method only handles successful results -

futureResult.foreach(result => println(s"Result: $result"))

You can also use combinators like map and flatMap. These are used to transform the result of a future -

val doubledResult: Future[Int] = futureResult.map(_ * 2)
doubledResult.foreach(result => println(s"Doubled Result: $result"))

Save the above program in Demo.scala. The following commands are used to compile and execute this program:

Command

\>scalac Demo.scala
\>scala Demo

Output

Doubled Result: 84

Composing Futures

You can compromise Futures to handle more asynchronous workflows. You can chain multiple futures together using flatMap and for-comprehensions -

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

val future1 = Future { 1 }
val future2 = Future { 2 }

val combinedFuture = for {
  result1 <- future1
  result2 <- future2
} yield result1 + result2

combinedFuture.foreach(result => println(s"Combined Result: $result"))

Save the above program in Demo.scala. The following commands are used to compile and execute this program:

Command

\>scalac Demo.scala
\>scala Demo

Output

Combined Result: 3

Error Handling

Error handling is important when you work with futures. Futures can fail, so you need to handle these failures. You can use methods like recover and recoverWith to handle errors -

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

val futureWithError: Future[Int] = Future { throw new RuntimeException("Failed!") }

val recoveredFuture = futureWithError.recover {
  case _: RuntimeException => 0
}

recoveredFuture.foreach(result => println(s"Recovered Result: $result"))

You can use recover method to provide a default value in case of an error. Whereas, recoverWith method is an alternative future -

val alternativeFuture = futureWithError.recoverWith {
  case _: RuntimeException => Future.successful(0)
}

Save the above program in Demo.scala. The following commands are used to compile and execute this program:

Command

\>scalac Demo.scala
\>scala Demo

Output

Recovered Result: 0

Waiting for Futures

You may need to wait for a future to complete in some cases. Blocking is generally discouraged. You can use Await to block and wait for a future result -

import scala.concurrent.Await
import scala.concurrent.duration._

val result = Await.result(futureResult, 5.seconds)
println(s"Result: $result")

Save the above program in Demo.scala. The following commands are used to compile and execute this program:

Command

\>scalac Demo.scala
\>scala Demo

Output

Result: 42

You can use blocking with caution. It can lead to performance issues and deadlocks.

Advanced Features

Promises

Promise is a writable and single-assignment container. Promise completes a future. You can use promises to create futures and complete these manually -

import scala.concurrent.Promise

val promise = Promise[Int]()
val future = promise.future

promise.success(42) // Completes the future with a value

You can use Promises for more control over the completion of futures.

Combining Multiple Futures

You can combine multiple futures using methods like zip, traverse, and sequence -

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

val future1 = Future { 1 }
val future2 = Future { 2 }

val combined = future1.zip(future2)
combined.foreach { case (result1, result2) => println(s"Combined: $result1, $result2") }

val listOfFutures = List(Future { 1 }, Future { 2 }, Future { 3 })
val futureOfList = Future.sequence(listOfFutures)
futureOfList.foreach(resultList => println(s"List of results: $resultList"))

Save the above program in Demo.scala. The following commands are used to compile and execute this program:

Command

\>scalac Demo.scala
\>scala Demo

Output

Combined: 1, 2
List of results: List(1, 2, 3)

Transforming Futures

There are various methods in Futures. These methods are used to transform their results, like map, flatMap, filter, collect, etc.. You can apply transformations and handle different cases -

val transformedFuture = futureResult.collect {
  case result if result > 0 => result * 2
}

Save the above program in Demo.scala. The following commands are used to compile and execute this program:

Command

\>scalac Demo.scala
\>scala Demo

Output

84

Notes

  • Futures handle asynchronous operations for non-blocking execution.
  • Future represents a value that will be available in the future. It succeeds with a value or fails with an exception.
  • Futures require an ExecutionContext to manage threads. It uses either the global context or a custom one.
  • You can use onComplete, foreach, and map callbacks to handle future results.
  • You can manage failures with recover and recoverWith methods to handle errors.
  • You can chain futures using flatMap and for-comprehensions for complex workflows.
  • You can use Promise to manually complete futures. It has more control over their completion.
Advertisements