Scala Concurrency: Futures, Promises, and ExecutionContext
Scala Concurrency: Futures, Promises, and ExecutionContext
Scala's Future[A] represents a value that will be computed asynchronously. Unlike Java threads or callbacks, Futures compose cleanly with map, flatMap, and for-comprehensions — making concurrent code readable and safe. This module covers everything you need to write concurrent Scala applications.
What Is a Future?
A Future[A] is a container for a value that may not be available yet. It starts computing immediately in a thread pool and completes with either a Success(value) or Failure(exception).
import scala.concurrent.{Future, Await}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
val f: Future[Int] = Future {
Thread.sleep(1000) // simulate work
42
}
println("Future started, doing other work...")
// Block and wait for result (only for examples/tests — avoid in production)
val result = Await.result(f, 5.seconds)
println(s"Result: $result") // Result: 42ExecutionContext
An ExecutionContext is the thread pool that runs your Futures. The global context wraps Java's ForkJoinPool:
import scala.concurrent.ExecutionContext.Implicits.global // global pool
// Custom context (for production — tune thread count to your workload)
import java.util.concurrent.Executors
import scala.concurrent.ExecutionContext
val ec = ExecutionContext.fromExecutorService(
Executors.newFixedThreadPool(8)
)Always be explicit about your ExecutionContext in production code rather than importing the global one.
Composing Futures with map and flatMap
Futures compose just like Option and List:
def fetchUser(id: Int): Future[String] = Future {
Thread.sleep(100)
s"User_$id"
}
def fetchOrders(user: String): Future[List[String]] = Future {
Thread.sleep(100)
List(s"Order1_$user", s"Order2_$user")
}
// Chain with flatMap
val orders: Future[List[String]] = fetchUser(1).flatMap(user => fetchOrders(user))
// Or with for-comprehension (cleaner)
val result: Future[List[String]] = for {
user <- fetchUser(1)
orders <- fetchOrders(user)
} yield orders
result.foreach(os => println(s"Orders: $os"))Running Futures in Parallel
Use Future.sequence or zip to run multiple Futures concurrently:
// These three Futures start immediately — they run in parallel
val f1 = fetchUser(1)
val f2 = fetchUser(2)
val f3 = fetchUser(3)
// Wait for all to complete
val all: Future[List[String]] = Future.sequence(List(f1, f2, f3))
// Wait for two specific Futures
val pair: Future[(String, String)] = f1.zip(f2)
// WRONG — this is sequential, not parallel:
val sequential = for {
u1 <- fetchUser(1) // starts, waits
u2 <- fetchUser(2) // starts only after u1 completes
} yield (u1, u2)Key rule: to run Futures in parallel, start them before the for-comprehension. Starting them inside <- makes them sequential.
Handling Errors
val riskyFuture: Future[Int] = Future {
if (scala.util.Random.nextBoolean()) 42
else throw new RuntimeException("Something went wrong")
}
// recover — handle failure, return a default
val safe: Future[Int] = riskyFuture.recover {
case e: RuntimeException => -1
}
// recoverWith — handle failure, return another Future
val recovered: Future[Int] = riskyFuture.recoverWith {
case e: RuntimeException => Future.successful(0)
}
// transform — handle both success and failure
import scala.util.{Success, Failure}
riskyFuture.onComplete {
case Success(value) => println(s"Got: $value")
case Failure(exception) => println(s"Failed: ${exception.getMessage}")
}Callbacks
Callbacks are side-effecting — use them for logging or side effects, not for composing results:
future.onComplete {
case Success(v) => println(s"Success: $v")
case Failure(e) => println(s"Error: ${e.getMessage}")
}
future.foreach(v => println(s"Value: $v")) // only on successPromises: Manual Completion
A Promise[A] is a writable Future[A]. You create the Promise, hand out its future, and complete it later:
import scala.concurrent.Promise
val promise = Promise[Int]()
val future: Future[Int] = promise.future
// Complete from another thread or callback
Future {
Thread.sleep(500)
promise.success(42)
// or: promise.failure(new Exception("oops"))
}
future.foreach(v => println(s"Promise resolved with: $v"))Promises are useful for bridging callback-based APIs to Future-based code.
Awaiting Results (Testing Only)
Await.result blocks the current thread — use only in tests or main methods:
import scala.concurrent.Await
import scala.concurrent.duration._
val result = Await.result(future, 10.seconds)
val result2 = Await.ready(future, 10.seconds) // returns Future (doesn't throw)In production code, chain callbacks or use onComplete instead of blocking.
Frequently Asked Questions
Q: When should I use Future vs Akka Actors for concurrency?
Use Future for independent, stateless asynchronous operations — HTTP calls, database queries, parallel computations. Use Akka Actors when you need to manage mutable state across concurrent operations, when you need message queuing, when dealing with distributed systems, or when you need supervision and fault tolerance. For most service-layer code, Futures are simpler and sufficient.
Q: Why should I avoid Await.result in production code?
Await.result blocks the calling thread until the Future completes. This defeats the purpose of asynchronous programming — you're tying up a thread that could be handling other requests. In a server application, blocking threads under load causes thread starvation and performance degradation. Instead, keep the chain asynchronous all the way up: return Future[T] from your methods and let the framework (Akka HTTP, Play, etc.) handle the final await.
Q: What happens if I forget to handle a Future's failure?
If a Future fails and you never call recover, recoverWith, onComplete, or failed, the exception is silently swallowed. This is a common source of bugs in Scala applications. Always handle the failure case. In production, configure ExecutionContext with a reporter for unhandled Future failures, and use logging in your onComplete handlers to ensure failures are visible.
Part of Scala Mastery Course — Module 19 of 22.
