Futures & Promises
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Scala 3.10 (3.10.0, released 2026-10-12) and every example was compiled and run with Scala
3.10.0 through Scala CLI on JDK 26 (checked on 2026-10-15). scala.concurrent.Future and Promise are part of the standard
library and have kept the same API since Scala 2.12, so the code also compiled and ran on the 3.9 LTS line (3.9.0, 2026-09-03)
and on 3.3 LTS (3.3.8); the small differences are in Scala 3.9 LTS versus 3.10. Examples that print timings or thread
names show the values observed on a 10-core machine; yours will differ.
A Future[A] is a handle to a value of type A that may not exist yet. You start a computation, get the handle back at once,
and describe what to do with the result. The same idea is CompletableFuture in Java and Deferred in Kotlin coroutines;
the comparison is in Comparison with Java and Kotlin. The examples use the trailhead scenario, in which a hiking toolkit
queries slow weather, crowd and snow services for a trail.
ExecutionContext: who runs the work
Almost every operation on a Future that runs user code takes an ExecutionContext as a context parameter (given/using
in Scala 3, implicit in Scala 2). An ExecutionContext is the abstraction over "somewhere to run a task"; Future itself
never creates threads. Forgetting to provide one is the first error everybody meets.
//> using scala 3.10.0
import scala.concurrent.Future
@main def run(): Unit =
val f = Future(1 + 1)
-- [E172] Type Error: e9.scala:5:23
5 | val f = Future(1 + 1)
| ^
|Cannot find an implicit ExecutionContext. You might add
|an (implicit ec: ExecutionContext) parameter to your method.
|
|The ExecutionContext is used to configure how and on which
|thread pools asynchronous tasks (such as Futures) will run,
|so the specific ExecutionContext that is selected is important.
...
|The following import might fix the problem:
|
| import scala.concurrent.ExecutionContext.Implicits.global
There are four common sources of an ExecutionContext.
| Source | What it is |
|---|---|
|
A shared fork-join pool sized to the number of processors. |
|
Wrap any Java |
|
Runs the task on the thread that completes the future, with no scheduling. Only for tiny, non-blocking, non-throwing callbacks such as |
A context you write |
Extend |
The next program runs the same kind of task on each of the first three and on parasitic, and prints where it ran.
//> using scala 3.10.0
import scala.concurrent.{Future, Await, ExecutionContext}
import scala.concurrent.duration.*
import java.util.concurrent.Executors
def where(label: String): String =
s"$label on ${Thread.currentThread.getName} virtual=${Thread.currentThread.isVirtual}"
@main def run(): Unit =
// 1. fixed pool
val pool = Executors.newFixedThreadPool(2)
given ExecutionContext = ExecutionContext.fromExecutorService(pool)
println(Await.result(Future(where("fixed")), 1.second))
// 2. virtual threads (JDK 21+)
val vexec = Executors.newVirtualThreadPerTaskExecutor()
val vec = ExecutionContext.fromExecutorService(vexec)
println(Await.result(Future(where("virtual"))(using vec), 1.second))
// 3. global
println(Await.result(Future(where("global"))(using ExecutionContext.global), 1.second))
// 4. parasitic runs on the thread that completes
val p = scala.concurrent.Promise[Int]()
val mapped = p.future.map { x => println(where("parasitic map")); x + 1 }(using ExecutionContext.parasitic)
p.success(1)
println(Await.result(mapped, 1.second))
vexec.shutdown()
pool.shutdown()
fixed on pool-1-thread-1 virtual=false
virtual on virtual=true
global on scala-execution-context-global-32 virtual=false
parasitic map on main virtual=false
2
Source: Scala API: ExecutionContext
Three details are visible in the output. A virtual thread has an empty name unless you give it one. The parasitic callback
ran on main, because main was the thread that called p.success(1). And an explicit (using ec) argument overrides the
given in scope, which is how a single call can opt out of the default.
An executor service that you create must be shut down, otherwise its non-daemon threads keep the JVM alive. The global context uses daemon threads and needs no shutdown, which is also why a program that only uses it can exit while futures are still running.
Virtual threads on JDK 21 and later
On JDK 21 and later, Executors.newVirtualThreadPerTaskExecutor() wrapped with ExecutionContext.fromExecutorService gives
every task its own virtual thread, as shown above. That suits code that blocks on I/O, because a blocked virtual thread is
cheap and the blocking hint (see Blocking and Await) is not needed. It does not make CPU-bound code faster, and it
does not change the Future API at all. See Java Virtual Threads for
the JVM side and the pinning pitfalls; Scala adds nothing on top of them for this use.
Creating a Future
Future.apply(body) takes its body by name, schedules it on the implicit ExecutionContext at once, and returns the handle.
The future is eager: it starts running as soon as you create it, whether or not anybody ever looks at the result.
//> using scala 3.10.0
import scala.concurrent.{Future, ExecutionContext}
import scala.concurrent.ExecutionContext.Implicits.global
def forecast(trail: String): Int =
Thread.sleep(200)
println(s"computing $trail on ${Thread.currentThread.getName}")
trail.length * 10
@main def run(): Unit =
println(s"main is ${Thread.currentThread.getName}")
val f: Future[Int] = Future(forecast("Alpina"))
println(s"isCompleted right away: ${f.isCompleted}, value: ${f.value}")
Thread.sleep(500)
println(s"isCompleted later: ${f.isCompleted}, value: ${f.value}")
main is main
isCompleted right away: false, value: None
computing Alpina on scala-execution-context-global-28
isCompleted later: true, value: Some(Success(60))
f.value is an Option[Try[A]]: None while the future is running, then Some(Success(a)) or Some(Failure(t)). The
Try is how a future carries an exception to whoever asks for the result. The Thread.sleep(500) in main is only there so
the program does not exit before the daemon-thread future finishes; the sections below show how to wait properly.
Other constructors do not need an execution context because no code runs asynchronously:
| Constructor | Result |
|---|---|
|
An already completed successful future. |
|
An already completed failed future. |
|
An already completed future from a |
|
A completed |
|
A future that never completes. Useful in tests and as the identity for races. |
|
A future that you complete by hand; see Promise. |
Because Future is eager and caches its result (the body ran exactly once in the next example), a val holding a future is
a running computation and not a recipe. This is the opposite of a lazy effect type such as IO or ZIO, discussed in
Parallel Collections & Concurrency Libraries.
//> using scala 3.10.0
import scala.concurrent.{Future, Await}
import scala.concurrent.duration.*
import scala.concurrent.ExecutionContext.Implicits.global
@main def run(): Unit =
var calls = 0
val f = Future { calls += 1; calls }
Await.result(f, 1.second); Await.result(f, 1.second)
println(s"body ran $calls time(s) after two awaits")
body ran 1 time(s) after two awaits
Callbacks
The lowest-level way to consume a future is to register a callback. onComplete receives the Try; foreach runs only for
success; there is no dedicated failure callback because onComplete and failed.foreach cover it.
//> using scala 3.10.0
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Success, Failure}
def elevation(trail: String): Int =
if trail.isEmpty then throw IllegalArgumentException("empty trail name")
trail.length * 100
@main def run(): Unit =
val ok = Future(elevation("Salewa"))
val bad = Future(elevation(""))
ok.onComplete:
case Success(v) => println(s"ok: $v")
case Failure(e) => println(s"ok failed: $e")
bad.onComplete:
case Success(v) => println(s"bad: $v")
case Failure(e) => println(s"bad failed: $e")
Thread.sleep(300)
ok: 600
bad failed: java.lang.IllegalArgumentException: empty trail name
Callbacks are registered on the implicit execution context and have three properties worth remembering:
-
A callback registered on an already completed future still runs, asynchronously, so there is no "missed it" race.
-
Several callbacks on the same future run in no guaranteed order, and may run concurrently.
-
A callback returns
Unit, so it cannot feed a value onward. As soon as you need the result of the callback, usemaporflatMapinstead.andThenis the one exception: it runs a side effect but returns the original future unchanged.
An exception thrown inside a foreach callback is not part of any future, because the callback’s result is discarded. It is
handed to ExecutionContext.reportFailure, which for the global context prints the stack trace and carries on:
//> using scala 3.10.0
import scala.concurrent.{Future, Await}
import scala.concurrent.duration.*
import scala.concurrent.ExecutionContext.Implicits.global
@main def run(): Unit =
val f = Future(1)
f.foreach(_ => throw RuntimeException("callback blew up"))
Thread.sleep(300)
println("main continues")
val r = for a <- Future(1); b <- Future(0) yield a / b
println(Await.ready(r, 1.second).value)
java.lang.RuntimeException: callback blew up
at e17$package$.run$$anonfun$1(e17.scala:9)
...
main continues
Some(Failure(java.lang.ArithmeticException: / by zero))
The first failure is printed to standard error and lost; the second, inside yield, is captured in the resulting future,
because yield is a map, which returns a future.
Transforming and combining: map, flatMap and for
map transforms the eventual value, flatMap chains a step that itself returns a future, and for/yield is syntax for the
two. Together they make a Future a monad: you write straight-line code, and the library arranges for each step to run when
the previous one completes. The same translation rules apply as in
For Comprehensions.
//> using scala 3.10.0
import scala.concurrent.{Future, Await}
import scala.concurrent.duration.*
import scala.concurrent.ExecutionContext.Implicits.global
def fetch(n: Int): Future[Int] = Future:
if n % 2 == 0 then throw RuntimeException(s"station $n offline")
n * 10
def show[A](f: Future[A]): String = Await.ready(f, 2.seconds).value.get.toString
@main def run(): Unit =
println(show(fetch(1).map(_ + 1)))
println(show(fetch(2).map(_ + 1)))
println(show(fetch(1).flatMap(a => fetch(a))))
println(show(fetch(1).zip(fetch(3))))
println(show(fetch(1).zip(fetch(2))))
println(show(fetch(2).failed))
println(show(fetch(1).filter(_ > 100)))
println(show(fetch(1).collect { case 10 => "ten" }))
println(show(Future.successful(1).andThen { case r => println(s"andThen saw $r") }))
Success(11)
Failure(java.lang.RuntimeException: station 2 offline)
Failure(java.lang.RuntimeException: station 10 offline)
Success((10,30))
Failure(java.lang.RuntimeException: station 2 offline)
Success(java.lang.RuntimeException: station 2 offline)
Failure(scala.concurrent.Future$$anon$2: Future.filter predicate is not satisfied)
Success(ten)
andThen saw Success(1)
Success(1)
Source: Scala 3 Book: Using map with Futures and Scala Documentation: Functional Composition and For-Comprehensions
Reading the results: a failure short-circuits map and flatMap (the function is never called and the same exception comes
out); fetch(1).flatMap(fetch) fails because the second call is fetch(10) and 10 is even; zip pairs two independent
futures and fails if either fails; failed flips the future so that the exception becomes the value; and filter and
collect turn a missed predicate into a failed future holding a NoSuchElementException. The show helper uses Await.ready only to
print the result in a demo.
The sequential-versus-parallel trap
A for over futures is sequential by construction: flatMap calls its function only after the previous future succeeds, so
a future that is created inside the comprehension does not start until its predecessors finish. To run independent calls
concurrently, create the futures first and combine them afterwards.
//> using scala 3.10.0
import scala.concurrent.{Future, Await}
import scala.concurrent.duration.*
import scala.concurrent.ExecutionContext.Implicits.global
def weather(trail: String): Future[Int] = Future { Thread.sleep(100); 12 }
def crowd(trail: String): Future[Int] = Future { Thread.sleep(100); 40 }
def snow(trail: String): Future[Int] = Future { Thread.sleep(100); 3 }
@main def run(): Unit =
val t0 = System.nanoTime
// sequential: each future is created inside the comprehension
val seq =
for
w <- weather("a")
c <- crowd("a")
s <- snow("a")
yield (w, c, s)
println(Await.result(seq, 2.seconds))
println(s"sequential ms: ${(System.nanoTime - t0) / 1_000_000}")
val t1 = System.nanoTime
// parallel: start all three first, combine after
val fw = weather("a")
val fc = crowd("a")
val fs = snow("a")
val par =
for
w <- fw
c <- fc
s <- fs
yield (w, c, s)
println(Await.result(par, 2.seconds))
println(s"parallel ms: ${(System.nanoTime - t1) / 1_000_000}")
(12,40,3)
sequential ms: 397
(12,40,3)
parallel ms: 107
Three 100 ms calls take about 400 ms (including warm-up) in the first form and about 100 ms in the second. The difference is
only where weather, crowd and snow are called: the diagram shows the second form, where three futures run at once and
the for waits on them in order.
A mistake in either form is easy to make because both read identically. A rule of thumb: if the right-hand side of a <-
is a method call that creates a future, the steps are sequential; if it is a previously defined val, they are concurrent.
The comprehension never starts anything; it only waits and combines.
Handling failures
A failed future carries a Throwable. The combinators below turn a failure back into a value or into another future, and are
the counterparts of catch in synchronous code.
//> using scala 3.10.0
import scala.concurrent.{Future, Await}
import scala.concurrent.duration.*
import scala.concurrent.ExecutionContext.Implicits.global
def fetch(n: Int): Future[Int] = Future:
if n % 2 == 0 then throw RuntimeException(s"station $n offline")
n * 10
def show[A](f: Future[A]): String = Await.ready(f, 2.seconds).value.get.toString
@main def run(): Unit =
println(show(fetch(2).recover { case _: RuntimeException => -1 }))
println(show(fetch(2).recoverWith { case _: RuntimeException => fetch(3) }))
println(show(fetch(2).fallbackTo(fetch(5))))
println(show(fetch(2).transform(identity, e => IllegalStateException("wrapped", e))))
println(show(Future.failed[Int](IllegalStateException("no"))))
Success(-1)
Success(30)
Success(50)
Failure(java.lang.IllegalStateException: wrapped)
Failure(java.lang.IllegalStateException: no)
Source: Scala API: Future
| Method | Purpose |
|---|---|
|
Replace a failure with a plain value. A |
|
Replace a failure with another future, for retries and fallbacks. |
|
If this fails, use |
|
Rewrite success and failure in one function. |
|
Two-function form, for changing the exception type while leaving values alone. |
|
Like |
|
A |
|
Run a side effect (logging, metrics) and pass the original result on. |
recover and recoverWith take partial functions, so Future(...).recover { case _: SomeError => ... } leaves unrelated
exceptions untouched. See Partial Functions.
Failures from the Scala Try machinery are the same ones used in Either & Try:
a future is, conceptually, an asynchronous Try. Converting between them with Future.fromTry and future.value is
occasionally useful in tests.
A retry helper
recoverWith is enough to write a retry function that reruns an operation a bounded number of times. The operation is passed
by name so that every attempt starts a fresh future.
//> using scala 3.10.0
import scala.concurrent.{Future, Await}
import scala.concurrent.duration.*
import scala.concurrent.ExecutionContext.Implicits.global
def retry[A](times: Int)(op: => Future[A]): Future[A] =
op.recoverWith { case _ if times > 1 => retry(times - 1)(op) }
@main def run(): Unit =
var attempts = 0
val flaky = retry(3):
Future:
attempts += 1
if attempts < 3 then throw RuntimeException(s"attempt $attempts failed") else s"ok after $attempts"
println(Await.result(flaky, 1.second))
ok after 3
Source: Scala API: Future
A production retry would also wait between attempts, which needs a timer; see Timeouts and cancellation.
Many futures at once
Working with a collection of futures is where the companion object helps. All of them need an implicit ExecutionContext
because the combination step runs as a callback.
//> using scala 3.10.0
import scala.concurrent.{Future, Await}
import scala.concurrent.duration.*
import scala.concurrent.ExecutionContext.Implicits.global
def level(n: Int): Future[Int] = Future:
Thread.sleep(100 * (4 - n))
if n == 3 then throw IllegalStateException("gauge 3 broken")
n * 10
@main def run(): Unit =
val ids = List(1, 2, 3, 4)
val healthy = ids.filter(_ != 3)
println(Await.result(Future.sequence(List(level(1), level(2))), 2.seconds))
println(Await.result(Future.traverse(healthy)(level), 2.seconds))
println(Await.ready(Future.sequence(ids.map(level)), 2.seconds).value)
val all = Future.traverse(ids)(i => level(i).transform(t => scala.util.Success(t)))
println(Await.result(all, 2.seconds))
println(Await.result(Future.firstCompletedOf(healthy.map(level)), 2.seconds))
println(Await.result(Future.foldLeft(healthy.map(level))(0)(_ + _), 2.seconds))
println(Await.result(Future.reduceLeft(healthy.map(level))(_ + _), 2.seconds))
println(Await.result(Future.find(healthy.map(level))(_ > 15), 2.seconds))
List(10, 20)
List(10, 20, 40)
Some(Failure(java.lang.IllegalStateException: gauge 3 broken))
List(Success(10), Success(20), Failure(java.lang.IllegalStateException: gauge 3 broken), Success(40))
40
70
70
Some(20)
| Method | Behavior |
|---|---|
|
Turns |
|
|
|
Completes with the first completed result, success or failure. The slower futures are not cancelled. |
|
The first successful value satisfying |
|
Combine results in the order of the input collection. |
|
The idiom for "keep every outcome": it converts a failure into a |
Note that Future.traverse over a very large collection starts every future immediately. To limit concurrency, process in
batches or use a library with bounded parallelism (see
Parallel Collections & Concurrency Libraries).
Promise
A Future is read-only. The writable side is a Promise: an object that you complete exactly once, from any thread, and
whose future is observed by others. Promises are how callback-based and event-based APIs are turned into futures, and how
you write your own combinators.
//> using scala 3.10.0
import scala.concurrent.{Future, Promise, Await}
import scala.concurrent.duration.*
import scala.concurrent.ExecutionContext.Implicits.global
import java.util.concurrent.TimeoutException
@main def run(): Unit =
val p = Promise[String]()
val f = p.future
println(f.isCompleted)
println(p.trySuccess("summit"))
println(p.trySuccess("again"))
println(f.value)
try p.success("third")
catch case e: IllegalStateException => println(s"IllegalStateException: ${e.getMessage}")
val q = Promise[Int]()
q.completeWith(Future(42))
println(Await.result(q.future, 1.second))
val slow = Promise[Int]().future
try Await.result(slow, 300.millis)
catch case e: TimeoutException => println(s"TimeoutException: ${e.getMessage}")
val z = Promise[Int]()
z.failure(new RuntimeException("boom"))
println(z.future.value)
println(Promise.successful(1).future.value)
false
true
false
Some(Success(summit))
IllegalStateException: Promise already completed.
42
TimeoutException: Future timed out after [300 milliseconds]
Some(Failure(java.lang.RuntimeException: boom))
Some(Success(1))
| Method | Behavior |
|---|---|
|
Complete the promise; throw |
|
Return |
|
Complete from another future’s outcome. |
|
Already completed promises. |
Because only one completer can win, a promise is a safe building block for a first-wins race. A promise that is never
completed gives a future that never completes, so Await.result on it reports a timeout, as in the example.
A typical use is to adapt a callback API. The following wraps a hypothetical listener-style service:
import scala.concurrent.{Future, Promise}
trait TrailService:
def load(name: String, onDone: Int => Unit, onError: Throwable => Unit): Unit
def loadLength(service: TrailService, name: String): Future[Int] =
val p = Promise[Int]()
service.load(name, p.success, p.failure)
p.future
This fragment was not run here because TrailService stands for a third-party callback API; the pattern is the standard way
to bridge such APIs. Use trySuccess/tryFailure if the library might call both handlers.
Blocking and Await
Await.result(future, timeout) and Await.ready(future, timeout) block the calling thread until the future completes. Both
require a finite Duration. result returns the value or rethrows the failure; ready returns the future itself, so you
inspect value.
Blocking is the one thing you should do rarely. It belongs in main, in a test, or at the boundary with a synchronous API;
never inside another future’s body and never inside a callback. If you must block inside a future, tell the execution
context with scala.concurrent.blocking.
//> using scala 3.10.0
import scala.concurrent.{Future, Await, blocking}
import scala.concurrent.duration.*
import scala.concurrent.ExecutionContext.Implicits.global
@main def run(): Unit =
val cores = Runtime.getRuntime.availableProcessors
println(s"cores: $cores")
def burst(label: String)(body: => Unit): Unit =
val t0 = System.nanoTime
val fs = (1 to cores * 2).map(_ => Future(body))
Await.result(Future.sequence(fs), 30.seconds)
println(s"$label: ${(System.nanoTime - t0) / 1_000_000} ms for ${cores * 2} sleeps of 500 ms")
burst("plain Thread.sleep ")(Thread.sleep(500))
burst("blocking { sleep } ")(blocking(Thread.sleep(500)))
cores: 10
plain Thread.sleep : 1032 ms for 20 sleeps of 500 ms
blocking { sleep } : 506 ms for 20 sleeps of 500 ms
The global pool has as many threads as cores, so twenty blocking sleeps on 10 cores need two rounds (about 1 s). Wrapping the
sleep in blocking { ... } lets the pool add compensating threads, and all twenty overlap (about 0.5 s). blocking only
informs the pool; it does nothing for a fixed pool, where the extra tasks queue. The Await methods call blocking
internally for the same reason.
Starvation and deadlock
Blocking inside a future on a bounded pool can deadlock. With a single-thread executor, an outer future that awaits an inner one holds the only thread, and the inner task can never run:
//> using scala 3.10.0
import scala.concurrent.{Future, Await, ExecutionContext}
import scala.concurrent.duration.*
import java.util.concurrent.Executors
@main def run(): Unit =
val one = Executors.newSingleThreadExecutor()
given ExecutionContext = ExecutionContext.fromExecutor(one)
val outer = Future:
val inner = Future(21)
Await.result(inner, 500.millis) * 2 // blocks the only thread the inner task needs
println(Await.ready(outer, 2.seconds).value)
one.shutdown()
Some(Failure(java.util.concurrent.TimeoutException: Future timed out after [500 milliseconds]))
The timeout is what saved the program; with Duration.Inf it would hang forever. Compose with flatMap instead of awaiting,
and keep the blocking calls out of the pool that runs the non-blocking work, for example by giving blocking I/O a separate
executor or a virtual-thread executor.
Timeouts and cancellation
Future has no built-in timeout combinator and no cancellation. Cancelling the observer does not stop the underlying work,
and a "timeout" only changes what the caller sees. The usual construction races the future against a timer through a
Promise:
//> using scala 3.10.0
import scala.concurrent.{Future, Promise, Await}
import scala.concurrent.duration.*
import scala.concurrent.ExecutionContext.Implicits.global
import java.util.concurrent.{Executors, TimeUnit, TimeoutException}
val timer = Executors.newSingleThreadScheduledExecutor: r =>
val t = Thread(r, "trailhead-timer")
t.setDaemon(true)
t
extension [A](f: Future[A])
def within(d: FiniteDuration): Future[A] =
val p = Promise[A]()
timer.schedule((() => p.tryFailure(TimeoutException(s"gave up after $d"))): Runnable, d.toMillis, TimeUnit.MILLISECONDS)
f.onComplete(p.tryComplete)
p.future
def slowFuture(ms: Long): Future[String] = Future { Thread.sleep(ms); "done" }
@main def run(): Unit =
println(Await.ready(slowFuture(50).within(500.millis), 2.seconds).value)
println(Await.ready(slowFuture(1500).within(300.millis), 2.seconds).value)
Some(Success(done))
Some(Failure(java.util.concurrent.TimeoutException: gave up after 300 milliseconds))
The extension method (see Extension Methods) wins the race with
tryFailure or tryComplete, whichever happens first. The slow task keeps running to completion in the background. If you
need to stop work, you must design for it: check an AtomicBoolean, interrupt a thread you own, or use a library whose
fibers or scopes are cancellable (see
Parallel Collections & Concurrency Libraries).
Fatal errors never complete a future
Future bodies catch only non-fatal exceptions. Future uses scala.util.control.NonFatal, which excludes
VirtualMachineError (so StackOverflowError and OutOfMemoryError), ThreadDeath, LinkageError, InterruptedException
and ControlThrowable. The treatment is not uniform, as the next program shows.
//> using scala 3.10.0
import scala.concurrent.{Future, Await}
import scala.concurrent.duration.*
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.boundary
def probe(label: String)(body: => Int): Unit =
val f = Future[Int](body)
val r =
try Await.ready(f, 700.millis).value.toString
catch case e: java.util.concurrent.TimeoutException => "never completed (TimeoutException from Await)"
println(f"$label%-22s -> $r")
@main def run(): Unit =
probe("RuntimeException")(throw RuntimeException("boom"))
probe("InterruptedException")(throw InterruptedException("stop"))
probe("NotImplementedError")(???)
probe("boundary.break")(boundary { boundary.break(5) })
probe("StackOverflowError")(throw StackOverflowError())
probe("OutOfMemoryError")(throw OutOfMemoryError("fake"))
probe("LinkageError")(throw LinkageError("fake"))
RuntimeException -> Some(Failure(java.lang.RuntimeException: boom))
InterruptedException -> Some(Failure(java.util.concurrent.ExecutionException: Boxed Exception))
NotImplementedError -> Some(Failure(java.util.concurrent.ExecutionException: Boxed Exception))
boundary.break -> Some(Success(5))
StackOverflowError -> never completed (TimeoutException from Await)
OutOfMemoryError -> never completed (TimeoutException from Await)
LinkageError -> never completed (TimeoutException from Await)
Source: Scala API: Future and Scala API: NonFatal
InterruptedException and NotImplementedError (???) are boxed in an ExecutionException so that they can be delivered
as a failure; ControlThrowable`s such as the one used by `boundary.break are converted into the break’s value; the three
JVM-level errors escape to the thread’s uncaught exception handler (the LinkageError stack trace printed to standard error
during the run is omitted above), and the future stays pending forever. The practical consequence: always give Await a
timeout and treat an unexplained hang as a possible fatal error in a future body.
Interoperating with CompletableFuture
scala.jdk.FutureConverters converts both ways. The Java side is what you get from Java libraries and from the JDK HTTP
client.
//> using scala 3.10.0
import scala.concurrent.{Future, Await}
import scala.concurrent.duration.*
import scala.concurrent.ExecutionContext.Implicits.global
import scala.jdk.FutureConverters.*
import java.util.concurrent.CompletableFuture
@main def run(): Unit =
val cf: CompletableFuture[String] = CompletableFuture.supplyAsync(() => "from java")
val sf: Future[String] = cf.asScala
println(Await.result(sf.map(_.toUpperCase), 1.second))
val back: CompletableFuture[Int] = Future(7).asJava.toCompletableFuture
println(back.get())
FROM JAVA
7
Source: Scala API: FutureConverters
asScala turns a CompletionStage into a Future; asJava returns a CompletionStage, and toCompletableFuture gives the
mutable class. Java-side callers see the full details in Calling Scala from Java.
Testing code that returns futures
Prefer returning the future from the test and letting the framework wait for it. MUnit does this out of the box: a test body
whose last expression is a Future is awaited and its failure fails the test.
//> using scala 3.10.0
//> using test.dep org.scalameta::munit::1.2.1
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
class FutureSuite extends munit.FunSuite:
test("a future returned from a test is awaited"):
Future(21 * 2).map(n => assertEquals(n, 42))
test("a failing future fails the test"):
Future(1).map(n => assertEquals(n, 2))
FutureSuite:
+ a future returned from a test is awaited 0.01s
==> X FutureSuite.a failing future fails the test 0.013s munit.ComparisonFailException: FutureSuite.test.scala:11
10: test("a failing future fails the test"):
11: Future(1).map(n => assertEquals(n, 2))
values are not the same
=> Obtained
1
=> Diff (- expected, + obtained)
-2
+1
Error: munit tests failed.
In plain code (a main, a script, a test framework without async support), Await.result(f, 5.seconds) is acceptable at
the edge. The test frameworks are covered in
MUnit Async, Fixtures & Other Frameworks.
To avoid real sleeps in tests of timing logic, inject the ExecutionContext and a scheduler.
Dependencies and build files
Future, Promise, Await and ExecutionContext are in the standard library, so neither a Scala CLI //> using dep
directive nor an sbt libraryDependencies entry is needed. The converters used above (scala.jdk.FutureConverters) are
also in the standard library since Scala 2.13.
Comparison with Java and Kotlin
-
Java’s
CompletableFutureis the closest analogue:thenApplyismap,thenComposeisflatMap,exceptionallyisrecover. ACompletableFutureis also eager, but it is mutable (anyone cancompleteit, which collapses Scala’sFutureandPromiseinto one class) and it can be cancelled in name only. Scala’s separation of the read-onlyFuturefrom the writablePromiseis the main design difference. Java’s modern alternatives are virtual threads and structured concurrency; see Java Virtual Threads. -
Kotlin uses coroutines, which are cancellable and structured, so a scope’s children are cancelled with the parent, and
suspendfunctions look sequential. A ScalaFuturehas none of that, but it also needs no compiler support and no coloured functions. See Kotlin Coroutines Basics. -
JavaScript’s
Promiseis eager too, but is single-threaded and runs callbacks on the event loop; a ScalaFutureruns on a thread pool and so needsExecutionContextand thread-safe shared state. -
Python’s
asynciotasks are cooperatively scheduled and cancellable; a ScalaFutureis preemptively scheduled by the pool and not cancellable.
Where the books are outdated
-
Material written for Scala 2.10 and 2.11 passes the context as
implicit ec: ExecutionContextand callsFuture { ... }with braces. Both still work in Scala 3, but idiomatic Scala 3 usesusingclauses and braceless blocks as shown here, andimport ExecutionContext.Implicits.globalis still the fastest way to get started. -
Older tutorials describe
onSuccessandonFailure. They were deprecated in Scala 2.12 and are gone from the 2.13 and 3 library; useforeach,onComplete,andThenandrecover. -
Books from the Scala 2.9 and 2.10 era that recommend
scala.actorsorscala.concurrent.ops.spawndescribe APIs that no longer exist in the current library. Actor-style programming today uses a library such as Pekko; see Parallel Collections & Concurrency Libraries.
Scala 3.9 LTS versus 3.10
The Future API is a standard-library matter. Its API is the Scala 2.13 one on every Scala 3 release; up to 3.7 the Scala
3 library depends on scala-library 2.13.x, and since 3.8 the same API is compiled by Scala 3 and versioned with the
compiler. Nothing in this page depends on the compiler version. The examples above were also
compiled and run on 3.3.8 and 3.9.0 with the same results. Two things are worth knowing:
-
Braceless
Future:blocks,extensionmethods andusingclauses need Scala 3.0 or later; on Scala 2.13 writeFuture { ... },implicit classandimplicit ec. The core program (for a <- Future(1); b <- Future(2) yield a + bwith anApp) compiled and printed3on Scala 2.13.18. -
The compiler error for the missing
ExecutionContextis E172 with the same wording on 3.3.8, 3.9.0 and 3.10.0.