Scala Concurrency

Scala runs on the JVM, so it has full access to Java threads, but it rarely uses them directly. The standard library gives you Future, a value that represents a result which is not ready yet, plus an ExecutionContext that decides which thread pool actually runs the work. Together they let you write concurrent code without manually creating and joining threads.

Threads

A raw JVM Thread is the lowest-level building block. You will see it, but rarely write it by hand — it is easy to leak, hard to compose, and gives you no way to return a value cleanly.

Example:

val worker = new Thread(() => {
  println(s"Running on ${Thread.currentThread().getName}")
})

worker.start()
worker.join() // wait for it to finish

Future

Future[A] represents a value of type A that will become available at some point, possibly on another thread. Creating a Future schedules the block to run asynchronously and returns immediately — the calling thread is never blocked.

Every Future needs an implicit ExecutionContext in scope: it is the thread pool the future runs on. The global one is fine for learning and for I/O-light work.

Example:

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

val slowSquare: Future[Int] = Future {
  Thread.sleep(200)
  21 * 2
}

println("Started, not waiting yet...")

slowSquare.foreach(result => println(s"Got: $result"))

Transforming Futures

A Future supports map, flatMap, and filter, exactly like Option and Try — which means it also composes inside a for-comprehension. Chained futures each run once their dependency completes, without any explicit callback nesting.

Example:

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

def fetchUser(id: Int): Future[String] = Future { s"user-$id" }
def fetchOrders(user: String): Future[Int] = Future { user.length * 3 }

val combined: Future[Int] = for {
  user   <- fetchUser(7)
  orders <- fetchOrders(user)
} yield orders

combined.foreach(n => println(s"Order count: $n"))

Handling Failure

A Future can fail: if the block throws, the failure is captured instead of crashing the thread. Handle it with recover, recoverWith, or by pattern matching on the eventual Try it wraps.

Example:

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

val risky: Future[Int] = Future(10 / 0)

risky.onComplete {
  case Success(value) => println(s"Value: $value")
  case Failure(ex)     => println(s"Failed: ${ex.getMessage}")
}

// or supply a fallback value instead of matching
val safe = risky.recover { case _: ArithmeticException => -1 }

Waiting For A Result

Production code stays asynchronous end to end and never blocks. But at the edge of a program — a main method, a test, a script — you sometimes need to block until a Future completes. Use Await.result with an explicit timeout.

Example:

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

val eventual = Future { 6 * 7 }
val answer = Await.result(eventual, 2.seconds)
println(answer) // 42
Beyond the standard library: for heavier concurrency — actors, streaming, structured supervision — the Scala ecosystem reaches for libraries like Akka/Pekko, ZIO, or Cats Effect. Future is the right tool to learn first because everything else builds on the same map/flatMap shape.