Futures and Promises in Scala

Explore when Scala `Future` is the right boundary-level abstraction, when `Promise` should appear, and where the model stops being enough.

Future: An eager handle to an eventual result running on an ExecutionContext.

Promise: The writable side used to complete a Future from the outside.

Future remains an important part of Scala because it is built into the standard library and many JVM APIs expose it directly. It works best at asynchronous boundaries: one call starts now, completes later, and produces exactly one result or failure. The model becomes weaker once you need structured cancellation, scoped resource cleanup, or complex workflow ownership.

Use Future For Boundary Asynchrony

Good Future use cases include:

  • wrapping one database or HTTP call
  • composing a small number of independent remote requests
  • integrating with libraries that already speak Future
  • exposing a lightweight asynchronous service boundary

That is different from treating Future as a full concurrency runtime. A Future begins eagerly as soon as it is created, and the standard model does not give you first-class cancellation of in-flight work.

Promise Is Mostly A Bridge Tool

Most application code should consume Future, not build Promise directly. Promise is useful when you need to adapt a callback-based API into a Future-based one.

 1import scala.concurrent.{ExecutionContext, Future, Promise}
 2
 3def fromCallback[A](
 4  register: (Either[Throwable, A] => Unit) => Unit
 5)(using ExecutionContext): Future[A] =
 6  val p = Promise[A]()
 7  register {
 8    case Right(value) => p.trySuccess(value)
 9    case Left(error)  => p.tryFailure(error)
10  }
11  p.future

That is a good fit because the callback boundary owns completion. If you find yourself creating Promise inside ordinary business logic just to sequence work, you are usually compensating for a design problem rather than solving one.

Composition Is Straightforward, But Execution Still Matters

Future composition is pleasant when the work is naturally one-shot:

 1import scala.concurrent.{ExecutionContext, Future}
 2
 3final case class User(id: String, name: String)
 4final case class Orders(total: Int)
 5final case class Dashboard(user: User, orders: Orders)
 6
 7def loadUser(id: String)(using ExecutionContext): Future[User] =
 8  Future.successful(User(id, "Ada"))
 9
10def loadOrders(id: String)(using ExecutionContext): Future[Orders] =
11  Future.successful(Orders(12))
12
13def loadDashboard(id: String)(using ExecutionContext): Future[Dashboard] =
14  for
15    user   <- loadUser(id)
16    orders <- loadOrders(id)
17  yield Dashboard(user, orders)

This reads cleanly, but the real behavior still depends on where the work runs. ExecutionContext is not incidental wiring. It is part of the design. CPU-bound work, blocking work, and externally asynchronous work should not all share the same execution assumptions.

ExecutionContext Is A Design Decision

The biggest operational mistake with Future is forgetting that scheduling policy lives outside the expression itself.

ConcernGood practiceCommon failure
CPU-bound workRun on a compute-oriented poolMix it with blocking I/O
Blocking I/OIsolate it on a dedicated poolBlock the global pool
Error recoveryRecover close to the boundary that understands the failureSwallow errors generically
TimeoutsApply them explicitly around the workflowAssume Future itself provides cancellation

If a Future body does Thread.sleep, blocking JDBC, or file I/O on the default global pool, you have already made the concurrency story worse even if the code still “works.”

Know Where Future Stops Being Enough

Future is often the right starting point, but it has real limits:

  • cancellation is not a normal part of the abstraction
  • ownership of spawned work is easy to hide
  • resource cleanup must be modeled indirectly
  • large asynchronous systems often need clearer supervision than callbacks alone provide

That is why Scala teams often keep Future at interoperability edges while using Cats Effect or ZIO for the core workflow model.

Common Failure Modes

Eager Work You Did Not Mean To Start Yet

Creating the Future starts the work immediately. That surprises teams who wanted a reusable description of work rather than immediate execution.

Blocking On The Wrong Pool

The code is technically asynchronous, but the underlying pool is starved because blocking work was mixed into it.

Using Promise As Workflow Glue

The code manually wires completion for logic that should have been expressed with ordinary composition or a higher-level effect model.

Practical Heuristics

Use Future for single-result asynchronous boundaries. Use Promise sparingly to adapt callback-style completion. Once you need structured cancellation, explicit resource lifetimes, or clearly owned background work, move up to an effect system instead of stretching Future beyond its design.

Knowledge Check

Loading quiz…
Revised on Thursday, April 23, 2026