Skip to content
AgileLogics
6 min read

Futures and Concurrency

Prerequisites: For-comprehensions

A Future[A] is a value that will be available later — the result of work running on another thread.

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

def fetchPrice(item: String): Future[Double] = Future {
  Thread.sleep(1000)
  item.length * 1.5
}

@main def futures(): Unit =
  val start = System.nanoTime()
  val a = fetchPrice("keyboard")
  val b = fetchPrice("mouse")
  val total = for
    x <- a
    y <- b
  yield x + y

  println(Await.result(total, 2.seconds))
  val elapsed = (System.nanoTime() - start) / 1_000_000
  println(elapsed < 1800)
19.5
true
  • Future { ... } starts the work immediately on a thread pool — the execution context (a given here, imported from Implicits.global).
  • a and b were started before the for-comprehension, so they ran in parallel: the total took about one second, not two. (Starting them inside the for would run them one after the other.)
  • map, flatMap, and for-comprehensions combine futures without blocking.
  • Await.result blocks — acceptable at the edge of a program (like main) or in tests, but avoid it elsewhere.

Many futures at once

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

def square(n: Int): Future[Int] = Future { Thread.sleep(100); n * n }

@main def traverse(): Unit =
  val all: Future[List[Int]] = Future.traverse(List(1, 2, 3, 4))(square)
  println(Await.result(all, 2.seconds))
List(1, 4, 9, 16)

Future.traverse (and Future.sequence) turn many futures into one future of a list, preserving order.

Failures

import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.*
import scala.util.{Failure, Success}

@main def failures(): Unit =
  val risky = Future { if true then throw IllegalStateException("service down") else 1 }
  val recovered = risky.recover { case e: IllegalStateException => -1 }

  println(Await.result(recovered, 1.second))
  Await.ready(risky, 1.second).value.get match
    case Success(v) => println(s"value $v")
    case Failure(e) => println(s"failed: ${e.getMessage}")
-1
failed: service down

A failed Future holds the exception; recover and recoverWith turn failures into values.

Limits of Future

Futures start running as soon as they're created, can't be cancelled, and mix describing work with running it. For complex concurrent systems, most Scala teams use an effect system — the next lesson.