Technology  /  Scala

🔴 Scala 21 guides · updated 2026

The JVM's hybrid OOP/functional language — immutability, pattern matching, and the type system that powers Spark and Akka, from first principles to production.

Futures and Concurrency in Scala: ExecutionContext and Composing Async Code

A Future in Scala represents a value that doesn’t exist yet — a computation running elsewhere, whose result you can act on once it completes, without blocking the current thread waiting for it. This guide covers Future’s core mechanics, the ExecutionContext every Future silently depends on, and the composition patterns (map, flatMap, Future.sequence) that let you build correct concurrent pipelines without manual thread management.


Creating and Using a Future

import scala.concurrent.{Future, ExecutionContext}
import scala.concurrent.ExecutionContext.Implicits.global
def fetchUser(id: String): Future[String] = Future {
Thread.sleep(1000) // simulating a slow network call
s"User-$id"
}
val userFuture: Future[String] = fetchUser("1")
userFuture.onComplete {
case scala.util.Success(user) => println(s"Got: $user")
case scala.util.Failure(ex) => println(s"Failed: ${ex.getMessage}")
}
println("This prints immediately, before the Future completes")

Future { ... } starts the block running asynchronously, on some thread other than the one that called it, and returns immediately with a Future[T] value representing “the result, once it’s ready.” This is why "This prints immediately" runs before the user is fetched — creating a Future doesn’t block; it schedules work and moves on. onComplete registers a callback that runs whenever the Future eventually finishes, succeeding with Success or failing with Failure — the same Success/Failure types from the Option/Either/Try guide, since a Future is conceptually “a Try that hasn’t finished yet.”


ExecutionContext — The Implicit Every Future Needs

import scala.concurrent.ExecutionContext.Implicits.global
def slowComputation(): Future[Int] = Future {
Thread.sleep(500)
42
}

Every Future operation requires an implicit (or given, in Scala 3 style) ExecutionContext in scope — this is the thread pool that actually executes the Future’s work, and it’s the exact same implicit-parameter mechanism covered in the previous two guides, just applied to a specific, ubiquitous library type. ExecutionContext.Implicits.global is a convenient shared thread pool suitable for most application code; production systems with specific performance requirements (I/O-heavy vs CPU-heavy workloads) often define a custom ExecutionContext sized and configured for their specific workload instead.

Future { computation }

Implicit ExecutionContext

(a thread pool)

Worker thread runs the computation

Future completes with Success or Failure

Registered callbacks (onComplete, map, etc.) run

A genuinely common mistake: forgetting to import an ExecutionContext produces a compile error (“could not find implicit value for parameter executor”) rather than a runtime failure — annoying the first time you hit it, but a direct instance of implicits catching a missing dependency at compile time rather than letting a Future silently never execute.


Composing Futures With map and flatMap

def fetchUser(id: String): Future[String] = Future { s"User-$id" }
def fetchOrders(user: String): Future[List[String]] = Future { List(s"$user-order1", s"$user-order2") }
val ordersFuture: Future[List[String]] = fetchUser("1").flatMap(user => fetchOrders(user))
val upperCaseUser: Future[String] = fetchUser("1").map(_.toUpperCase)

Future supports the identical map/flatMap chaining vocabulary as Option, Either, and Try — for exactly the reason covered in the for-comprehensions guide: any type providing map/flatMap with compatible signatures works with the same chaining style and inside for comprehensions. flatMap here is essential because fetchOrders itself returns a Future — without flatMap, chaining would produce a Future[Future[List[String]]], the exact “nested wrapper” problem flatMap exists to flatten, as covered for collections and Option earlier in this series.

// The same composition, written as a for comprehension — often clearer for 3+ steps
val result: Future[String] = for {
user <- fetchUser("1")
orders <- fetchOrders(user)
} yield s"$user has ${orders.length} orders"

The Sequential-Await Trap — The Single Most Common Future Mistake

// SLOW — each Await.result blocks the current thread until that specific Future finishes,
// so the two independent fetches run one after another instead of concurrently
import scala.concurrent.Await
import scala.concurrent.duration._
val user = Await.result(fetchUser("1"), 5.seconds)
val orders = Await.result(fetchOrders(user), 5.seconds)
// FAST — both Futures are CREATED immediately, so they start running concurrently right away
val userFuture = fetchUser("1")
val secondUnrelatedFuture = fetchSomethingElse() // starts running in parallel, immediately
val combined: Future[(String, String)] = for {
user <- userFuture
other <- secondUnrelatedFuture
} yield (user, other)

This mirrors the exact “sequential await mistake” covered from the JavaScript/TypeScript side elsewhere on this site: calling Await.result (or, in async/await-style code, sequentially awaiting) forces the current thread to block and wait for one Future to finish before the next one is even created — turning what could be two concurrent operations into two sequential ones. The fix is the same principle in both ecosystems: create every independent Future you’ll need first (which starts them all running concurrently), and only combine/await their results afterward.

// Future.sequence — running many independent Futures concurrently and collecting all results
val userIds = List("1", "2", "3")
val allUsersFuture: Future[List[String]] = Future.sequence(userIds.map(fetchUser))
// All three fetchUser calls run concurrently; allUsersFuture completes once ALL of them do

Future.sequence turns a List[Future[T]] into a single Future[List[T]] — critically, the individual Futures inside the list were already created (and therefore already running concurrently) by the .map(fetchUser) call before sequence ever touches them; sequence only combines their eventual results, it doesn’t control when they start.


Error Handling

def riskyOperation(): Future[Int] = Future {
if (scala.util.Random.nextBoolean()) 42
else throw new RuntimeException("Something went wrong")
}
val handled: Future[Int] = riskyOperation().recover {
case _: RuntimeException => 0 // fallback value on failure
}
val handledWith: Future[Int] = riskyOperation().recoverWith {
case _: RuntimeException => Future.successful(-1) // fallback via another Future
}
riskyOperation().onComplete {
case scala.util.Success(value) => println(s"Got $value")
case scala.util.Failure(ex) => println(s"Error: ${ex.getMessage}")
}

An exception thrown inside a Future { ... } block is automatically caught and converted into a Failure, never crashing the calling thread — this is a real, deliberate safety property distinguishing Future from a raw thread, where an uncaught exception in the thread’s run method would otherwise be lost or crash silently depending on the thread’s uncaught-exception handler. recover/recoverWith mirror Try’s identically-named methods (covered in the Option/Either/Try guide) for exactly the same reason — a Future[T] really is “a Try[T] that hasn’t resolved yet,” and the parallel method names reflect that relationship directly.


Awaiting a Result — Mostly for Tests and Application Entry Points

import scala.concurrent.Await
import scala.concurrent.duration._
val result = Await.result(fetchUser("1"), 5.seconds)
println(result)

Await.result blocks the calling thread until the Future completes (or throws a TimeoutException after the specified duration) — genuinely necessary at your application’s actual entry point (a main method needs to eventually produce a real, synchronous result) or in test code verifying a Future’s outcome, but it should almost never appear inside application business logic itself, where blocking a thread defeats the entire point of using Future in the first place.


A Realistic Composed Pipeline

case class User(id: String, name: String)
case class Order(id: String, amount: Double)
def fetchUser(id: String): Future[User] = Future { User(id, s"User-$id") }
def fetchOrders(userId: String): Future[List[Order]] = Future {
List(Order("O1", 50.0), Order("O2", 75.0))
}
def calculateLoyaltyDiscount(orders: List[Order]): Future[Double] = Future {
if (orders.map(_.amount).sum > 100) 0.1 else 0.0
}
def processCustomer(userId: String): Future[String] = for {
user <- fetchUser(userId)
orders <- fetchOrders(user.id)
discount <- calculateLoyaltyDiscount(orders)
total = orders.map(_.amount).sum * (1 - discount)
} yield s"${user.name}: total $${total} (${(discount * 100).toInt}% loyalty discount)"
processCustomer("1").foreach(println)

This mirrors the validation-pipeline pattern from the for-comprehensions guide exactly, but composing Futures instead of Eithers — sequential steps that genuinely depend on each other’s results (you need user.id before you can fetch that user’s orders) are naturally expressed with for/flatMap, while steps that don’t depend on each other should still be created independently and combined afterward, per the sequential-await warning above.


Racing Futures and Timeouts

import scala.concurrent.Future
def primaryService(): Future[String] = Future { Thread.sleep(2000); "primary result" }
def backupService(): Future[String] = Future { Thread.sleep(500); "backup result" }
val fastest: Future[String] = Future.firstCompletedOf(List(primaryService(), backupService()))
// completes with "backup result" — whichever Future finishes first, wins

Future.firstCompletedOf runs several Futures concurrently and completes as soon as any one of them finishes — a genuinely useful pattern for racing a primary and fallback service, or for implementing a manual timeout by racing the real operation against a Future that simply sleeps for the timeout duration and then fails.

The Danger of Blocking Inside a Future

// DANGEROUS — Thread.sleep (or any blocking call) inside a Future ties up a thread pool thread doing NOTHING
def badExample(): Future[String] = Future {
Thread.sleep(5000) // this thread is now blocked and unusable for 5 full seconds
"done"
}
// Better: use a dedicated ExecutionContext for blocking operations, separate from the main compute pool
import java.util.concurrent.Executors
val blockingEc: ExecutionContext = ExecutionContext.fromExecutor(Executors.newCachedThreadPool())
def betterExample(): Future[String] = Future {
Thread.sleep(5000)
"done"
}(blockingEc)

A thread pool has a finite number of threads — if enough Futures block (via Thread.sleep, a blocking database driver call, or Await.result nested inside another Future), the pool can run out of available threads entirely, causing unrelated Futures elsewhere in the application to silently queue up and stall. This is a genuinely common production issue (“thread pool starvation”), and the standard mitigation is isolating known-blocking operations onto their own dedicated ExecutionContext, keeping the main pool free for non-blocking, CPU-light async work.

Common Pitfalls

SymptomLikely cause
”could not find implicit value for parameter executor”Missing import scala.concurrent.ExecutionContext.Implicits.global (or a custom ExecutionContext) in scope
Two independent operations run slower than expected, back to backThe sequential-await trap — both Futures should be created before either is awaited/flatMapped on
The whole application becomes sluggish under loadThread pool starvation from blocking calls inside Future blocks on the shared global pool — isolate blocking work onto a dedicated ExecutionContext
A Future chain’s error seems to vanish silentlyForgot to attach recover/recoverWith/onComplete — an unhandled Future failure doesn’t crash your program, it’s simply never observed
Await.result throws TimeoutException unexpectedlyThe duration passed was too short for the actual operation, or the operation is stuck behind thread pool starvation elsewhere

Frequently Asked Questions

Why does every Future operation need an implicit ExecutionContext? Because a Future’s work has to run somewhere — on some actual thread, from some thread pool — and ExecutionContext is that abstraction. Requiring it as an implicit parameter (rather than hardcoding a single global thread pool inside Future itself) lets different parts of an application use differently-configured thread pools for different workload types.

What’s the difference between Future.successful(value) and Future { value }? Future.successful(value) creates an already-completed Future immediately, with no actual asynchronous work or thread pool involvement — useful for returning a Future-typed value you already have synchronously, matching an API’s expected return type. Future { value } schedules the block to run asynchronously on the ExecutionContext, even if the block itself is trivial.

Is Future the same as a Java CompletableFuture? Conceptually similar (both represent an eventual asynchronous result with composition methods), but Scala’s Future integrates natively with map/flatMap/for-comprehensions and the Try-based success/failure model, giving it a more consistent feel alongside Option/Either/Try than direct interop with CompletableFuture’s API would.

Why is the sequential-await pattern so easy to write by accident? Because writing Await.result (or await in other languages’ async/await syntax) for each step in sequence often mirrors how you’d naturally write synchronous code — the mistake is subtle precisely because the code still “looks right” and produces correct results, just slower than necessary, since it doesn’t actually change what’s computed, only how much time it takes.

Does Future guarantee anything about which thread callbacks run on? No — callbacks (onComplete, map, flatMap) run on whatever thread the ExecutionContext schedules them on, which is not guaranteed to be the same thread that created the Future. This is a deliberate design choice supporting genuine concurrency, but it means you shouldn’t rely on thread-local state persisting across a Future chain’s steps.


What’s Next

Future composition, built on the same map/flatMap vocabulary as Option/Either/Try, is the standard way to write concurrent Scala code without manual thread management. This closes out the advanced-language section of this series. Next: Testing in Scala, covering ScalaTest and MUnit — including how the pure-function-friendly design from the immutability guide makes testing meaningfully simpler.