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.

Leave a Comment

Your email address will not be published. Required fields are marked *