async-getting-started.md
August 19, 2026 · View on GitHub
import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem';
Welcome! This tutorial introduces Async[A], a zero-allocation effect type from ZIO Blocks that unifies ready values, failures, and genuinely suspended computations under a single type and combinator set. If you know basic Scala syntax and have a sense of what an effect type is, you have everything you need to follow along.
1. Introduction
By the end of this tutorial, you will be able to:
- Create ready-value effects with
Async.succeedand transform them withAsync#mapandAsync#block. - Handle errors with
Async.fail,Async#catchAll, andAsync#either. - Write sequential async code in direct style using
Async.async { … .await … }. - Bridge throw-based code and callback-based APIs with
Async.attemptandAsync.promise/Completer. - Fork background computations with
Async#startand cancel them withAsync#cancel.
To add the async module to your project, include this dependency in your build.sbt:
libraryDependencies += "dev.zio" %%% "zio-blocks-async" % "@VERSION@"
Then bring the full DSL into scope at the top of each file you use it from:
import zio.blocks.async._
We recommend reading from top to bottom — each section builds directly on the one before it.
2. Background: What Is Async[A]?
Async[A] was designed to solve a specific problem: code often juggles three different kinds of values — results that are already available, computations that need to wait for I/O or a callback, and failures. Treating these differently in different places creates friction. Async[A] is one abstraction that covers all three.
The design's most important property is its happy-path allocation budget: zero. When you chain Async.succeed, Async#map, and another map call together, every step is a plain function call. No wrapper objects accumulate on the heap. Only a computation that truly suspends — waiting for a callback, a timer, or a thread — leaves a pending object behind. This is the zero-allocation promise: you pay for suspension only when you actually suspend.
We will explore Async[A] through an order-processing scenario: looking up a user via a callback API, parsing an order ID, checking stock availability in the background, and recovering gracefully from any failure — one concept at a time.
3. Ready Values: Async.succeed, map, and block
The simplest async computation is one that already has its answer. Async.succeed(value) wraps an available value into an Async[A] so it can take part in async chains without allocating anything. Once you have an Async, you transform it with Async#map and drive it to its final result with Async#block.
Here we wrap the integer 42, double it with map, then extract the result:
import zio.blocks.async._
val result: Int = Async.succeed(42).map(_ * 2).block
println(s"Ready mapped: $result")
Expected output:
Ready mapped: 84
Async.succeed(42)wraps42as a ready-valueAsync[Int]with zero allocation.- Calling
mapwith_ * 2applies the function; because the input is already ready, the entire chain is just a function call — no suspension object is created. - The
blockcall is the eager driver — it polls the async until it completes and returns the final value. It is safe to call on the JVM; on Scala.js it throws if the computation is still suspended.
Try changing 42 to a different number and watch the output change accordingly.
4. Error Handling: Async.fail, catchAll, and either
Not every computation succeeds. Async.fail(throwable) creates a failed async that short-circuits all downstream map calls without invoking their functions. Async#catchAll recovers by applying a function that returns a new Async.
Here we create a failed computation and recover from it with catchAll:
import zio.blocks.async._
val recovered: String = Async.fail(new Exception("oops"))
.catchAll(_ => Async.succeed("default"))
.block
println(s"Recovered: $recovered")
Expected output:
Recovered: default
Async.fail(new Exception("oops"))creates a computation that carries the exception as its failure.- Calling
catchAllwith a recovery function intercepts the failure; the lambda ignores the specific error and returns a ready-value replacement. - The
blockcall drives the recovered chain to its result,"default".
Sometimes you want to observe both the success and failure branches as plain data rather than handle them immediately. Async#either reifies both outcomes as Either[Throwable, A] so you can pattern-match on them:
import zio.blocks.async._
val observed: Either[Throwable, Int] = Async.fail(new Exception("error"))
.either
.block
println(s"Observed: $observed")
Expected output:
Observed: Left(java.lang.Exception: error)
- Calling
eitherwraps the failure inLeft; a successful result would appear inRight, turning the async's outcome into an ordinary Scala value. - The
blockcall materialises thatEitherso the learner can inspect it.
5. Direct Style: Async.async and await
Writing nested Async#flatMap chains is precise but becomes hard to read when many steps depend on each other. Async.async { … } lets you write that same sequencing in direct style: inside the block, call Async#await on any Async to extract its value and bind it to a local variable, as if you were writing straight-line code. The compiler rewrites every await call into a flatMap chain at compile time, so the runtime behaviour is identical.
Here we compose a user name and an order ID without a single explicit flatMap:
import zio.blocks.async._
val summary: String = Async.async {
val user = Async.succeed("Ada").await
val order = Async.succeed(9001).await
s"${user}'s order ${order}"
}.block
println(s"Summary: $summary")
Expected output:
Summary: Ada's order 9001
Async.async { … }opens a macro-powered block; the entire expression produces anAsync[String].- Calling
awaitonAsync.succeed("Ada")extracts"Ada"and binds it touser; this is not a blocking call — the macro rewrites it into aflatMapcontinuation. - Calling
awaitonAsync.succeed(9001)similarly binds the order number toorder. - The final string expression becomes the block's result value.
- The
blockcall drives the whole composed async to completion.
:::caution[await Is Only Valid Inside Async.async { … }]
The await method is enforced by the compiler to be used only inside an Async.async { … } block. On Scala 3 the inline expansion fails with:
".await may only be used directly inside an Async.async { ... } block."
On Scala 2, the @compileTimeOnly annotation fires at the same point. Try deleting the Async.async { … } wrapper — the compiler will tell you immediately.
:::
6. Bridging Exceptions: Async.attempt
Scala code often signals failure by throwing exceptions rather than returning error values. Async.attempt(body) evaluates a block that may throw and captures any exception as an async failure, turning it into a value that catchAll can recover. A block that succeeds produces a ready-value Async; one that throws produces a failed Async.
The example parses a well-formed string and then a malformed one:
import zio.blocks.async._
val good: Int = Async.attempt("42".toInt).block
println(s"Parsed: $good")
val bad: Either[Throwable, Int] = Async.attempt("oops".toInt).either.block
println(s"Failed parse: $bad")
Expected output:
Parsed: 42
Failed parse: Left(java.lang.NumberFormatException: For input string: "oops")
Async.attempt("42".toInt)evaluates"42".toInt; because it succeeds, the result42becomes a ready-valueAsync[Int].Async.attempt("oops".toInt)evaluates"oops".toInt; theNumberFormatExceptionis caught and becomes a failedAsync.- Chaining
eitherandblockdrives the failed async and reifies its outcome asLeft(…).
7. Callback Bridging: Async.promise and Completer
Many real-world APIs — database drivers, network libraries, timers — signal completion by calling a callback rather than returning a value. Async.promise lets you lift these APIs into Async without rewriting them. It suspends the computation and provides a Completer[A] — a thread-safe, one-shot handle — that you can pass to the callback. Calling completer.succeed(value) from any thread resolves the async and wakes up any awaiter.
The example starts a background thread that completes the promise after a short delay:
import zio.blocks.async._
val result: String = Async.promise[String] { c =>
new Thread {
override def run(): Unit = {
Thread.sleep(10)
c.succeed("hello from callback")
}
}.start()
}.block
println(s"Promise resolved: $result")
import zio.blocks.async._
val result: String = Async.promise[String] {
val c = summon[Completer[String]]
new Thread {
override def run(): Unit = {
Thread.sleep(10)
c.succeed("hello from callback")
}
}.start()
}.block
println(s"Promise resolved: $result")
Expected output:
Promise resolved: hello from callback
Async.promise[String] { … }opens the promise body; on Scala 2 theCompleter[String]arrives as an explicit parameterc; on Scala 3 it arrives as a context function argument retrieved withsummon[Completer[String]].- We capture the completer in
cso it can be referenced from the Thread'srun()method — implicits and givens do not propagate across thread boundaries, so we capture explicitly. - Calling
c.succeed("hello from callback")completes the promise; the first call wins and subsequent calls are no-ops. - The
blockcall waits until the completer fires and returns the resolved value.
8. Forking and Cancellation: start and Async.Running
By default, async chains run eagerly on the calling thread until the first suspension or completion. To drive a computation on a background worker (a separate JVM thread or a Scala.js microtask queue entry) instead, call Async#start on any Async. This returns an Async.Running[A] handle — itself an Async[A] — representing the in-flight computation. You can join it by calling block on the handle, or stop it early with cancel.
The first block forks a computation and joins it:
import zio.blocks.async._
val running: Async.Running[Int] = Async.succeed(42)
.map { x => println(s"Running in background: $x"); x * 2 }
.start
val joined: Int = running.block
println(s"Joined: $joined")
Expected output:
Running in background: 42
Joined: 84
- Calling
startforks the entire chain —Async.succeed(42)plus themap— onto a background worker; the calling thread continues immediately. running.blockblocks the calling thread until the background computation finishes and returns the result84.- The background computation prints its message before returning the value, so that line appears first.
The companion method Async.start forks a plain expression. The following block demonstrates cancellation:
import zio.blocks.async._
val running2: Async.Running[Int] = Async.start { Thread.sleep(100); 99 }
running2.cancel()
println("Cancelled running2")
Expected output:
Cancelled running2
Async.start { Thread.sleep(100); 99 }forks the block onto a background worker; the call returns theRunninghandle immediately.- Calling
cancelstops the driver loop; if cancellation linearises before the computation finishes, the result is never published to any awaiter. The call is idempotent.
9. Putting It Together
Let's combine all six concepts into a single order-processing pipeline. The program bridges a callback-based user-lookup API with Async.promise, safely parses an order ID with Async.attempt, runs a stock check in the background with start, sequences everything in direct style inside Async.async, and recovers from any failure with catchAll:
10. Running the Examples
Clone the repository and move into its root directory:
git clone https://github.com/zio/zio-blocks.git
cd zio-blocks
Each concept's standalone example is shown below. Expand a section to see the source and the command to run it.
Concept 1: Ready Values
Run it with:
sbt "async-examples/runMain zio.blocks.async.gettingstarted.ReadyValuesExample"
Concept 2: Error Handling
Run it with:
sbt "async-examples/runMain zio.blocks.async.gettingstarted.ErrorHandlingExample"
Concept 3: Direct Style
Run it with:
sbt "async-examples/runMain zio.blocks.async.gettingstarted.DirectStyleExample"
Concept 4: Bridging Exceptions
Run it with:
sbt "async-examples/runMain zio.blocks.async.gettingstarted.AttemptExample"
Concept 5: Callback Bridging
Run it with:
sbt "async-examples/runMain zio.blocks.async.gettingstarted.CallbackBridgeExample"
Concept 6: Forking and Cancellation
Run it with:
sbt "async-examples/runMain zio.blocks.async.gettingstarted.ForkingExample"
Complete Example: Order Processing Pipeline
Run it with:
sbt "async-examples/runMain zio.blocks.async.gettingstarted.CompleteExample"
11. What You've Learned
By completing this tutorial, you can now:
- Create ready-value effects with
Async.succeed, transform them withmap, and drive them to a result withblock. - Handle failures with
Async.fail, recover them withcatchAll, and observe both outcomes as data witheither. - Write sequential async pipelines in direct style using
Async.async { … .await … }— the compiler threads theflatMapcalls for you. - Lift throw-based Scala code safely into the async error channel with
Async.attempt. - Bridge a callback-based API with
Async.promiseandCompleter, fork background work withstart, and hold a cancellableAsync.Runninghandle.
12. Where to Go Next
The Async reference page documents every method and combinator with full signatures — it is the natural next stop once you are comfortable with the basics in this tutorial.
If your application manages resources that need deterministic cleanup — database connections, file handles, or connection pools — read the Compile-Time Resource Safety with Scope tutorial, which shows how to tie resource lifetimes to lexical scopes and compose them without try/finally boilerplate.