Futures and asynchronous code

Scala · Advanced · 15 min

Services do I/O concurrently; Future is the standard library's basic tool for it.

A Future[A] represents a value that will be available later, computed on a thread pool. Future { work } starts the computation immediately on an ExecutionContext, which you supply as a using parameter; scala.concurrent.ExecutionContext.Implicits.global is a reasonable default for CPU-bound work.

Futures compose without blocking:

  • map, flatMap and for comprehensions chain dependent steps.
  • Future.sequence(list) and Future.traverse turn many futures into one; zip combines two.
  • recover and recoverWith handle failures; a failed future carries the exception, and later steps are skipped.
  • onComplete registers a callback with a Try.

Futures are eager: two futures created before a for run in parallel, while futures created inside it run sequentially. Start independent work first, then combine.

Never block inside a future-based service. Await.result(f, 5.seconds) exists for tests and the top of main; elsewhere return the Future. Wrap blocking I/O in blocking { ... } so the global pool can compensate.

Futures can't be cancelled and they are not referentially transparent (creating one starts work). Larger codebases often use effect systems such as Cats Effect or ZIO, whose lazy IO values add cancellation, resource safety and structured concurrency. Java 21 virtual threads are a simpler alternative for blocking-style code.

Example

import scala.concurrent.{Future, Await, blocking}
import scala.concurrent.duration.*
import scala.concurrent.ExecutionContext.Implicits.global

def fetchPrice(item: String): Future[Double] = Future {
  blocking(Thread.sleep(200))                    // simulate blocking I/O
  if item == "unknown" then throw IllegalArgumentException(item)
  item.length * 1.5
}

val start = System.nanoTime
val fa = fetchPrice("apple")                     // both start now
val fb = fetchPrice("kiwi")
val total = for a <- fa; b <- fb yield a + b
println(Await.result(total, 2.seconds))          // 13.5
println((System.nanoTime - start) / 1_000_000 < 380)  // true: ran in parallel

val all = Future.traverse(List("fig", "plum"))(fetchPrice)
println(Await.result(all, 2.seconds))            // List(4.5, 6.0)

val safe = fetchPrice("unknown").recover { case _: IllegalArgumentException => 0.0 }
println(Await.result(safe, 2.seconds))           // 0.0

Remember

  • Future { ... } starts immediately on an ExecutionContext
  • Compose with map/flatMap/for, traverse and recover
  • Create independent futures before the for comprehension to run them in parallel
  • Await only in tests and at the program edge

Watch out for

  • Creating futures inside a for comprehension and expecting them to run concurrently; each starts after the previous completes.

Check yourself

  1. val r = for a <- Future(slowA()); b <- Future(slowB()) yield a + b. How do slowA and slowB run?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.scala-lang.org/overviews/core/futures.html

Builds on

Unlocks

Nothing builds on this yet.