# scala-async turns a block of awaits into a state machine at compile time

> scala-async is a Scala DSL that lets you write `async { ... }` blocks with `await` inside them, and a compiler plugin transforms those blocks into non-blocking state machines so the code reads like sequential code. The restrictions are strict: an await cannot be nested inside a lambda or inside a try block, and the mechanism depends on a macro over scala-reflect.

**scala/scala-async** — An asynchronous programming facility for Scala

- Repository: https://github.com/scala/scala-async
- Stars: 1,162 · Forks: 84
- Language: Scala
- License: Apache-2.0
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/scala-scala-async

## The dependency and the compiler flag

The setup has two parts and both are required. You add the library, and you enable the compiler support. As of scala-async 1.0, Scala 2.12.12 or 2.13.3 or later are required. In sbt the dependency is added with the `%%` operator so the Scala binary version suffix is handled for you, and the compiler option is added with `scalacOptions += "-Xasync"`:

```scala
libraryDependencies += "org.scala-lang.modules" %% "scala-async" % "1.0.1"
libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value % Provided

scalacOptions += "-Xasync"
```

The `scala-reflect` dependency at Provided scope is not incidental, because the transformation is a macro that runs at compile time and needs the reflection API on the compiler classpath. The Maven example is the same thing in XML, with the artifact id carrying the `_2.13` suffix and the `scala-maven-plugin` receiving the `-Xasync` argument. So the project is not a library you drop in, it is a library plus a compiler plugin, and the flag is what tells the compiler to rewrite the blocks.

## The shape of the code you write

With the flag on, the code reads sequentially:

```scala
import scala.concurrent.ExecutionContext.Implicits.global
import scala.async.Async.{async, await}

val future = async {
  val f1: Future[Boolean] = async { ...; true }
  val f2 = async { ...; 42 }
  if (await(f1)) await(f2) else 0
}
```

The `async` keyword marks a block of asynchronous code, and such a block usually contains one or more `await` calls, which mark a point at which the computation will be suspended until the awaited `Future` is complete. By default, `async` blocks operate on `scala.concurrent.{Future, Promise}`, and the documentation notes the system can be adapted to alternative implementations of the Future pattern. The key property is that the call to an async block returns immediately, so the code inside runs on the execution context rather than on the caller's thread.

## What the compiler actually does

The documentation walks through a four line example and explains the transformation step by step, and this is where the design becomes clear. A method `slowCalcFuture` returns a `Future`. An `async` block that contains `await` calls is analysed during compilation and transformed into non-blocking code, with the `await` points becoming suspensions in a generated state machine. Control flow passes immediately to the line that consumes the result, since the computation in the async block is not executed on the caller's thread. The `await` triggers the future and suspends until it is calculated, and only then does the next one start. The documentation is careful about a distinction that people often get wrong: while the lines are non-blocking, they are not parallel.

## Non-blocking is not parallel, and the code shows the difference

The example that starts the two futures and then awaits them is the parallel version, and the documentation contrasts it with the sequential one. Writing it as two `await` expressions on the same line means the first computation completes before the second is triggered, because the await suspends until the future is calculated and only then does the next await start. To parallelise, you have to bind both futures to values first, so that both are in flight before either is awaited:

```scala
def combined: Future[Int] = async {
  val future1 = slowCalcFuture
  val future2 = slowCalcFuture
  await(future1) + await(future2)
}
```

The difference is two lines of code, and the fact that the library makes it visible rather than hiding it is a design point. It also means the transformation does not reorder your code for you, which is the behaviour you want in a library that rewrites control flow.

## The restrictions are the price of the transformation

The limitations section lists two, and both are compile-time errors rather than runtime surprises. The `await` must be directly in the control flow of the async expression, so it cannot be nested under a local method, object, class or lambda, with the invalid example being a `foreach` over a list whose body awaits. The `await` must also not be nested within `try`, `catch` or `finally`, and the documentation notes that this implementation restriction may be lifted in future versions. The reason for both is the same: the transformation has to be able to see the await points statically in order to build the state machine, and a lambda or a try block introduces a boundary it cannot rewrite through.

## Why not just use map and flatMap

The documentation closes by comparing with the direct Future API, and gives the same computation in for-comprehension form, using a for with a `<-` generator to bind the two results. The `async` approach is said to have two advantages. The code more directly reflects the programmer's intent and does not require naming the results `r1` and `r2`, and the advantage is more pronounced when control structures are mixed inside async blocks. The second advantage is about code size: async blocks are compiled to a single anonymous class, as opposed to a separate anonymous class for each closure required at each generator in the for-comprehension, which reduces the size of the generated code and can avoid boxing of intermediate results. That second point is the one that matters for a library, since it is a per-callsite cost in a large codebase.

## Where the project stands

The repository is a standard Scala project with a `build.sbt`, a `project/` directory for the sbt build definition, a `src/` directory, a `NOTICE` file alongside the `LICENSE`, a `.jvmopts` file, and a code of conduct and contributing guide. The licence is Apache 2.0. The release history shows the project reaching 1.0.0 in July 2021 with a 1.0.1 patch in September of the same year, after a 1.0.0-M1 milestone in 2020, and the last push to the main branch was on 2026-09-05. That gap between the last release and the last commit is the practical detail to keep in mind: the artifact you depend on is from 2021, and the 2.12 and 2.13 builds in the Maven Central badges are what a new project will pull.

## Conclusion

Adopt scala-async if you are on Scala 2.12 or 2.13 and want sequential-looking code that composes futures, since the alternative is a for-comprehension or a chain of flatMap calls that obscures the intent. Do not adopt it if you are on Scala 3, because the plugin is tied to the 2.x compiler and the scala-reflect macro, and it requires a compiler flag. Verify first that your awaits sit directly in the control flow of the async block, because nesting one inside a lambda or a try block is rejected at compile time, and remember that a compiler plugin in the build is a dependency you will have to keep working.

## FAQ

### What does scala-async do?

It is a Scala DSL that enables a direct style of coding when composing futures. The `async` block marks a block of asynchronous code containing `await` calls, and the compiler plugin transforms those blocks into non-blocking code.

### How do I set up scala-async?

Add the `scala-async` dependency and the `scala-reflect` dependency at Provided scope, then enable the compiler support by adding `-Xasync` to the compiler options, with `scalacOptions += "-Xasync"` in sbt. Scala 2.12.12 or 2.13.3 or later is required.

### Where can I not put an await in scala-async?

An await must be directly in the control flow of the async expression. It cannot be nested under a local method, object, class or lambda, and it must not be nested within a try, catch or finally block.

### Does scala-async run computations in parallel?

Not by default. The documentation is explicit that the code is non-blocking but not parallel, and shows the two-line rearrangement of binding both futures to values first so that they are in flight before either is awaited.

### What licence is scala-async released under?

The Apache 2.0 licence, with the LICENSE and NOTICE files in the repository root. The current release is 1.0.1 from 2021-09-09, and the last push to the main branch was on 2026-09-05.

## Sources

- [Issues](https://github.com/scala/scala-async/issues)
- [License: Apache-2.0](https://github.com/scala/scala-async/blob/main/LICENSE)
- [README](https://github.com/scala/scala-async/blob/main/README.md)
- [Releases](https://github.com/scala/scala-async/releases)
- [scala/scala-async on GitHub](https://github.com/scala/scala-async)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/scala-scala-async
