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,flatMapandforcomprehensions chain dependent steps.Future.sequence(list)andFuture.traverseturn many futures into one;zipcombines two.recoverandrecoverWithhandle failures; a failed future carries the exception, and later steps are skipped.onCompleteregisters a callback with aTry.
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.0Remember
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.