Scala Futures
A Future represents a computation that runs in the background and produces a result at some point in the future. Instead of waiting for a slow database query or network request to finish before moving on, your program can start the operation and continue doing other work. When the result is ready, the Future delivers it.
The Real-World Analogy
Imagine you order food at a restaurant. The waiter gives you a receipt and walks away to place your order. You do not stand at the counter waiting — you sit down, check your phone, and chat with friends. When the food is ready, the waiter brings it to you. The receipt is like a Future: it is a promise that something will arrive, and you can keep working while you wait.
Without Future (blocking) With Future (non-blocking)
────────────────────────── ─────────────────────────────
Start task 1 Start task 1 ─┐
Wait... Start task 2 ─┤ all run
Wait... Start task 3 ─┤ in parallel
Task 1 done Task 3 done │
Start task 2 Task 1 done ─┘
Wait... Task 2 done
Task 2 done Total: max(t1,t2,t3)
Total: t1 + t2 + t3 (much faster)
Setup: ExecutionContext
Futures need a thread pool — a collection of background threads — to run on. This is provided by an ExecutionContext. Import the global one for learning purposes:
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Success, Failure}
Creating a Future
val myFuture: Future[Int] = Future {
// This block runs in a background thread
Thread.sleep(1000) // simulate a slow operation
42
}
println("This prints immediately, before Future completes")
The code inside Future { ... } starts running in a background thread right away. The line after the Future definition does not wait for it to finish.
Handling the Result with onComplete
import scala.util.{Success, Failure}
val future = Future {
10 * 10
}
future.onComplete {
case Success(value) => println(s"Got result: $value")
case Failure(exception) => println(s"Failed: ${exception.getMessage}")
}
Thread.sleep(500) // wait for future to complete in demo
Output:
Got result: 100
onComplete is a callback — code you register to run when the Future finishes. It receives a Try[A] which is either Success(value) or Failure(exception).
Transforming Futures with map
Use map to transform the result of a Future without blocking:
val priceInDollars: Future[Double] = Future {
Thread.sleep(300)
100.0 // fetch price from API
}
val priceInRupees: Future[Double] = priceInDollars.map(_ * 83.5)
priceInRupees.onComplete {
case Success(price) => println(f"Price in INR: ₹$price%.2f")
case Failure(e) => println(s"Error: ${e.getMessage}")
}
Thread.sleep(1000)
Future[Double] .map(_ * 83.5) Future[Double]
(100.0 in $) ────────────────────▶ (8350.0 in ₹)
Chaining Futures with flatMap
When the second Future depends on the result of the first, use flatMap:
def fetchUserId(name: String): Future[Int] = Future {
Thread.sleep(200)
if name == "Alice" then 1 else 2
}
def fetchUserScore(userId: Int): Future[Int] = Future {
Thread.sleep(200)
userId * 100
}
val result: Future[Int] =
fetchUserId("Alice").flatMap(id => fetchUserScore(id))
result.onComplete {
case Success(score) => println(s"Score: $score") // Score: 100
case Failure(e) => println(s"Error: $e")
}
Thread.sleep(1000)
For Comprehension with Futures
Chained flatMap calls become hard to read. Use for comprehension for clarity:
def getProductPrice(id: String): Future[Double] = Future {
Thread.sleep(100); 299.99
}
def getTaxRate(region: String): Future[Double] = Future {
Thread.sleep(100); 0.18
}
def getShipping(weight: Double): Future[Double] = Future {
Thread.sleep(100); if weight > 5 then 99.0 else 49.0
}
val totalCost: Future[Double] =
for
price <- getProductPrice("LAPTOP01")
taxRate <- getTaxRate("IN")
shipping <- getShipping(3.5)
yield price + (price * taxRate) + shipping
totalCost.onComplete {
case Success(cost) => println(f"Total: ₹$cost%.2f")
case Failure(e) => println(s"Error: $e")
}
Thread.sleep(2000)
// Total: ₹452.99 (approx)
Running Futures in Parallel
The for comprehension above runs each future sequentially (each waits for the previous one). To run them in parallel, start all futures first, then collect results:
// Sequential (slower: waits one by one)
val slow =
for
a <- Future { Thread.sleep(500); 1 }
b <- Future { Thread.sleep(500); 2 }
yield a + b
// Parallel (faster: all start at the same time)
val f1 = Future { Thread.sleep(500); 1 }
val f2 = Future { Thread.sleep(500); 2 }
val fast =
for
a <- f1
b <- f2
yield a + b
fast.onComplete {
case Success(sum) => println(s"Sum: $sum") // Sum: 3
case Failure(e) => println(e)
}
Thread.sleep(1000)
Sequential: F1 ──── F2 ──── Total: ~1000ms
Parallel: F1 ────
F2 ──── Total: ~500ms
Future.sequence: List of Futures to Future of List
val productIds = List("P1", "P2", "P3")
val priceFutures: List[Future[Double]] =
productIds.map(id => Future { Thread.sleep(200); id.length * 100.0 })
val allPrices: Future[List[Double]] = Future.sequence(priceFutures)
allPrices.onComplete {
case Success(prices) => println(prices) // List(200.0, 200.0, 200.0)
case Failure(e) => println(e)
}
Thread.sleep(1000)
Recovering from Failures
Use recover to handle failures gracefully and provide a fallback value:
val riskyFuture: Future[Int] = Future {
throw new RuntimeException("Database connection failed")
}
val safeFuture: Future[Int] = riskyFuture.recover {
case e: RuntimeException =>
println(s"Caught error: ${e.getMessage}")
-1 // fallback value
}
safeFuture.onComplete {
case Success(v) => println(s"Value: $v") // Value: -1
case Failure(e) => println(s"Still failed: $e")
}
Thread.sleep(500)
Awaiting a Future (Blocking — Use Sparingly)
In production code, you chain Futures and register callbacks. But in tests or scripts, you sometimes need to block and wait for a result:
import scala.concurrent.Await
import scala.concurrent.duration._
val future = Future { 42 }
val result = Await.result(future, 5.seconds) // blocks up to 5 seconds
println(result) // 42
Do not use Await in production web servers or APIs — blocking threads kills performance. Use it only in main methods, tests, or scripts where blocking is acceptable.
Future Summary
Operation Purpose
──────────────── ──────────────────────────────────────
Future { } Run code in background
.map(f) Transform result without blocking
.flatMap(f) Chain another Future
.onComplete { } Handle success or failure
.recover { } Provide fallback on failure
Future.sequence() Combine List[Future] → Future[List]
Await.result() Block and get value (use sparingly)
Futures form the foundation of concurrent programming in Scala. Libraries like Akka, Play Framework, and Spark all build on this abstraction to handle thousands of simultaneous operations without blocking threads.
