@zio.dev/zio-blocks 0.0.51 → 0.0.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/adr/2026-07-18-data-migration.md +123 -0
  2. package/guides/async-getting-started.md +687 -0
  3. package/guides/compile-time-resource-safety-with-scope.md +6 -0
  4. package/guides/getting-started-with-mux.md +0 -112
  5. package/guides/query-dsl-extending.md +1 -1
  6. package/guides/query-dsl-fluent-builder.md +1 -1
  7. package/guides/query-dsl-reified-optics.md +1 -1
  8. package/guides/query-dsl-sql.md +395 -1
  9. package/guides/sql-checked-interpolation.md +173 -0
  10. package/guides/sql-transactions.md +286 -0
  11. package/guides/telemetry-guide.md +131 -70
  12. package/guides/zio-schema-migration.md +6 -6
  13. package/index.md +200 -559
  14. package/package.json +1 -1
  15. package/reference/async.md +1379 -531
  16. package/reference/chunk.md +3 -3
  17. package/reference/codegen/index.md +1 -1
  18. package/reference/combinators.md +4 -4
  19. package/reference/config/config-decoder.md +460 -0
  20. package/reference/config/config-source.md +489 -0
  21. package/reference/config/errors.md +278 -0
  22. package/reference/config/flags.md +369 -0
  23. package/reference/config/formats.md +314 -0
  24. package/reference/config/index.md +304 -0
  25. package/reference/config/rollout.md +336 -0
  26. package/reference/context.md +6 -49
  27. package/reference/data-migration.md +269 -0
  28. package/reference/datastar/attributes.md +302 -0
  29. package/reference/datastar/events.md +234 -0
  30. package/reference/datastar/index.md +256 -0
  31. package/reference/datastar/signals.md +230 -0
  32. package/reference/datastar/sse.md +295 -0
  33. package/reference/datastar.md +2 -2
  34. package/reference/docs.md +2 -2
  35. package/reference/endpoint/bulk-creation.md +96 -0
  36. package/reference/endpoint/endpoint.md +1 -0
  37. package/reference/endpoint/index.md +9 -89
  38. package/reference/endpoint/path-codec.md +12 -24
  39. package/reference/endpoint/route-pattern.md +4 -6
  40. package/reference/endpoint/segment-codec.md +19 -32
  41. package/reference/html.md +313 -9
  42. package/reference/htmx/index.md +4 -52
  43. package/reference/htmx/response-headers.md +240 -0
  44. package/reference/http-model/headers.md +735 -0
  45. package/reference/http-model/index.md +3 -1
  46. package/reference/http-model/model.md +107 -71
  47. package/reference/http-model/schema-codecs.md +522 -0
  48. package/reference/http-model/schema.md +6 -3
  49. package/reference/http-model/server-sent-event.md +341 -0
  50. package/reference/jwt.md +195 -0
  51. package/reference/maybe.md +128 -11
  52. package/reference/media-type.md +2 -2
  53. package/reference/mux.mdx +7 -2
  54. package/reference/openapi.md +3 -3
  55. package/reference/projection.md +654 -0
  56. package/reference/resource-management/index.md +1 -1
  57. package/reference/resource-management/resource.md +2 -98
  58. package/reference/resource-management/scope.md +1 -209
  59. package/reference/resource-management/wire.md +4 -50
  60. package/reference/ringbuffer/advanced.mdx +1 -1
  61. package/reference/ringbuffer/index.mdx +3 -3
  62. package/reference/ringbuffer/mpmc.mdx +38 -4
  63. package/reference/ringbuffer/mpsc.mdx +36 -4
  64. package/reference/ringbuffer/spmc.mdx +1 -1
  65. package/reference/ringbuffer/spsc.mdx +87 -15
  66. package/reference/schema/allows.md +0 -96
  67. package/reference/schema/binding.md +2 -2
  68. package/reference/schema/built-in-codecs/avro.md +2 -2
  69. package/reference/schema/built-in-codecs/bson.md +50 -20
  70. package/reference/schema/built-in-codecs/csv.md +2 -2
  71. package/reference/schema/built-in-codecs/index.md +3 -3
  72. package/reference/schema/built-in-codecs/json/index.md +2 -2
  73. package/reference/schema/built-in-codecs/json/json.md +1 -0
  74. package/reference/schema/built-in-codecs/messagepack.md +3 -3
  75. package/reference/schema/built-in-codecs/thrift.md +2 -2
  76. package/reference/schema/built-in-codecs/toon.md +3 -3
  77. package/reference/schema/built-in-codecs/yaml.md +2 -2
  78. package/reference/schema/codec.md +11 -11
  79. package/reference/schema/dynamic-optic.md +48 -3
  80. package/reference/schema/dynamic-schema.md +3 -3
  81. package/reference/schema/index.md +2 -0
  82. package/reference/schema/path-interpolator.md +2 -0
  83. package/reference/schema/reflect-transformer.md +140 -0
  84. package/reference/schema/schema-evolution/as.md +4 -4
  85. package/reference/schema/schema-evolution/into.md +2 -2
  86. package/reference/schema/schema-expr.md +2 -2
  87. package/reference/schema/schema-search.md +263 -0
  88. package/reference/schema/schema.md +10 -2
  89. package/reference/schema/type-class-derivation.md +1 -1
  90. package/reference/smithy.md +502 -3
  91. package/reference/sql/db-codec-deriver.md +3 -3
  92. package/reference/sql/db-codec.md +22 -22
  93. package/reference/sql/db-con.md +4 -4
  94. package/reference/sql/db-connection.md +1 -1
  95. package/reference/sql/db-param.md +1 -1
  96. package/reference/sql/db-result-reader.md +4 -2
  97. package/reference/sql/db-tx.md +46 -14
  98. package/reference/sql/ddl.md +1 -1
  99. package/reference/sql/frag.md +44 -10
  100. package/reference/sql/index.md +7 -7
  101. package/reference/sql/repo.md +15 -15
  102. package/reference/sql/sql-dialect.md +1 -1
  103. package/reference/sql/sql-logger.md +1 -1
  104. package/reference/sql/sql-name-mapper.md +3 -3
  105. package/reference/sql/table-metadata.md +3 -3
  106. package/reference/sql/table.md +10 -10
  107. package/reference/sql/transactor-zio.md +1 -1
  108. package/reference/sql/transactor.md +21 -11
  109. package/reference/sql-zio.md +2 -2
  110. package/reference/streams/core/index.md +32 -0
  111. package/reference/streams/{pipeline.md → core/pipeline.md} +210 -74
  112. package/reference/streams/{sink.md → core/sink.md} +331 -353
  113. package/reference/streams/{stream.md → core/stream.md} +919 -209
  114. package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
  115. package/reference/streams/execution-and-compatibility/index.md +35 -0
  116. package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
  117. package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
  118. package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
  119. package/reference/streams/index.md +140 -67
  120. package/reference/streams/primitives/index.md +30 -0
  121. package/reference/streams/primitives/reader.md +1992 -0
  122. package/reference/streams/{writer.md → primitives/writer.md} +254 -98
  123. package/reference/telemetry/common/any-value.md +90 -0
  124. package/reference/telemetry/common/attribute-key.md +87 -0
  125. package/reference/telemetry/common/attributes.md +118 -0
  126. package/reference/telemetry/common/index.md +39 -0
  127. package/reference/telemetry/common/instrumentation-scope.md +24 -0
  128. package/reference/telemetry/common/resource.md +34 -0
  129. package/reference/telemetry/index.md +311 -0
  130. package/reference/telemetry/logging/index.md +197 -0
  131. package/reference/telemetry/logging/log-enrichment.md +72 -0
  132. package/reference/telemetry/logging/log-formatter.md +100 -0
  133. package/reference/telemetry/logging/log-record-processor.md +56 -0
  134. package/reference/telemetry/logging/log-record.md +44 -0
  135. package/reference/telemetry/logging/log-writer.md +64 -0
  136. package/reference/telemetry/logging/logger-provider.md +142 -0
  137. package/reference/telemetry/logging/logger.md +83 -0
  138. package/reference/telemetry/logging/severity.md +62 -0
  139. package/reference/telemetry/metrics/index.md +150 -0
  140. package/reference/telemetry/metrics/instruments.md +183 -0
  141. package/reference/telemetry/metrics/labeled-instruments.md +74 -0
  142. package/reference/telemetry/metrics/meter-provider.md +76 -0
  143. package/reference/telemetry/metrics/meter.md +98 -0
  144. package/reference/telemetry/metrics/metric-data.md +57 -0
  145. package/reference/telemetry/otel/custom-exporter.md +216 -0
  146. package/reference/telemetry/otel/index.md +212 -0
  147. package/reference/telemetry/tracing/index.md +155 -0
  148. package/reference/telemetry/tracing/sampler.md +89 -0
  149. package/reference/telemetry/tracing/span-builder.md +57 -0
  150. package/reference/telemetry/tracing/span-context.md +39 -0
  151. package/reference/telemetry/tracing/span-data.md +32 -0
  152. package/reference/telemetry/tracing/span-kind.md +55 -0
  153. package/reference/telemetry/tracing/span-processor.md +53 -0
  154. package/reference/telemetry/tracing/span-status.md +47 -0
  155. package/reference/telemetry/tracing/span.md +117 -0
  156. package/reference/telemetry/tracing/tracer-provider.md +91 -0
  157. package/reference/telemetry/tracing/tracer.md +52 -0
  158. package/reference/typeid.md +0 -64
  159. package/sidebars.js +365 -185
  160. package/undocumented-report.md +528 -270
  161. package/reference/config.md +0 -158
  162. package/reference/streams/concurrent-operators.md +0 -106
  163. package/reference/streams/reader.md +0 -1284
  164. package/reference/streams/scala-2-compatibility.md +0 -55
  165. package/reference/streams/zero-boxing.md +0 -275
  166. package/reference/telemetry.md +0 -693
@@ -1,651 +1,1499 @@
1
1
  ---
2
2
  id: async
3
3
  title: "Async"
4
+ description: "Reference for the ZIO Blocks async module: Async[A], Pollable, Completer, Async.Running, and Cancelable."
5
+ keywords:
6
+ - "Asynchronous Effects"
7
+ - "Eager Evaluation"
8
+ - "Callback Bridge"
9
+ - "Structured Cancellation"
10
+ - "Async"
11
+ - "Pollable"
4
12
  ---
5
13
 
6
- The `async` module provides `Async[A]`, a lightweight, zero-dependency
7
- asynchronous effect type for modern Scala. It is designed around a single idea:
8
- **a ready `Async[A]` is just an `A`**. The happy path allocates nothing — no
9
- effect tree, no wrapper, no boxing beyond what a generic JVM method already
10
- requires — so synchronous code composed with `map` / `flatMap` runs at
11
- hand-written speed while still being able to suspend on genuinely asynchronous
12
- work.
14
+ import Tabs from '@theme/Tabs';
15
+ import TabItem from '@theme/TabItem';
16
+
17
+ The `async` module provides `Async[A]` — a small asynchronous effect type for Scala 2.13 and Scala 3, targeting both JVM and Scala.js.
18
+
19
+ An `Async[A]` value is a computation that either yields an `A` or fails with a `Throwable`. Unlike a lazy effect type, building one *runs* it: the synchronous work happens as you construct it, and only a computation that genuinely has to wait for something is left pending. [Evaluation model](#evaluation-model) explains where that line falls.
20
+
21
+ Conceptually, an `Async[A]` is one of three things — a value that is already available, a failure that has already happened, or a computation that will complete later:
22
+
23
+ ```scala
24
+ // The mental model, not the real encoding.
25
+ enum Async[+A]:
26
+ case Ready(value: A) // already available
27
+ case Failed(cause: Throwable) // already failed
28
+ case Suspended(source: Pollable[A]) // completes later, via a callback
29
+ ```
30
+
31
+ The real definition is a type alias whose runtime representation is `Any`. That is a performance decision, not a modelling one: a ready value *is* its own `Async`, so the common path allocates nothing and boxes nothing. Reach for the three cases above when reasoning about behaviour, and let the encoding stay invisible — no combinator in this module requires you to know it.
32
+
33
+ Suspension is where the other types enter. A `Pollable[A]` is the extension point that produces a not-yet-available result, [`Completer`](#completer) is the ready-made `Pollable` for bridging callbacks, [`Async.Running`](#asyncrunning) is a `Pollable` you can also cancel, and [`Cancelable`](#cancelable) is that cancellation interface on its own.
34
+
35
+ The type is aimed at infrastructure-level code: places that need a first-class asynchronous value, on both the JVM and Scala.js, without taking on an ecosystem to get one. Where a fuller effect system offers an environment type, a typed error channel and fibers, `Async` offers a computation, a `Throwable`, and a handle you can cancel — and in exchange stays cheap enough to use on paths that are usually synchronous.
36
+
37
+ ## Installation
38
+
39
+ Add the module to your build:
40
+
41
+ ```scala
42
+ libraryDependencies += "dev.zio" %% "zio-blocks-async" % "0.0.56"
43
+ ```
44
+
45
+ In a cross-built project, use `%%%` so the same line resolves for both JVM and Scala.js:
46
+
47
+ ```scala
48
+ libraryDependencies += "dev.zio" %%% "zio-blocks-async" % "0.0.56"
49
+ ```
50
+
51
+ The module publishes for JVM and Scala.js, on Scala 2.13 and Scala 3. That single coordinate is all you add: it brings `zio-blocks-combinators` with it, along with `dotty-cps-async` on Scala 3 or `scala-reflect` on Scala 2, both of which power the direct-style rewrite.
13
52
 
14
53
  ## Overview
15
54
 
16
- `Async[A]` is an opaque type whose ready representation is the value itself and
17
- whose pending representation is a `Pollable[A]`. You never construct it
18
- directly; you enter the type through constructors and transform it through
19
- extension methods:
20
-
21
- - **Constructors** — `Async.succeed`, `Async.fail`, `Async.attempt`,
22
- `Async.never`, `Async.collectAll`, and the callback bridge `Async.promise`.
23
- - **Transformers** — `map`, `flatMap`, `zip`, `zipWith`, `catchAll`,
24
- `mapError`, `orElse`, `foldCause`, `either`, `tap`, `ensuring`, `as`, `unit`,
25
- `*>`, `<*`, `flatten`, and the conditional helpers `when` / `unless`.
26
- - **Direct style** — `Async.async { ... .await ... }` lets you write
27
- straight-line code with `.await`, rewritten at compile time into a
28
- non-blocking `flatMap` chain.
29
- - **Running** — `.block` drives an `Async` to its value (blocking on the JVM,
30
- throwing on a genuinely pending value on JS).
31
- - **Interop** — conversions to and from `scala.concurrent.Future` on every
32
- platform, Java's `CompletionStage` / `CompletableFuture` on the JVM, and
33
- `js.Promise` on Scala.js.
34
-
35
- On Scala 3 the transformers are zero-cost `inline` extension methods (the ready
36
- path applies your function directly to the underlying value with no `Function1`
37
- allocation). On Scala 2 they are methods on an implicit `AsyncOps` class. The
38
- raw-value representation — a ready `Async[A]` *is* an `A` — holds identically on
39
- both.
40
-
41
- ## Runnable example
42
-
43
- The [`async-examples`](https://github.com/zio/zio-blocks/tree/main/async-examples)
44
- module contains a single self-contained program that walks through the major
45
- features in one file — ready-path composition, direct-style `Async.async` /
46
- `.await`, `zip` / `collectAll`, error handling, `Async.promise`, a custom
47
- [[Pollable]] leaf, `tap` / `ensuring`, cancellable `Async.start` /
48
- `Async.Running`, and JVM `Future` interop.
49
-
50
- ```bash
51
- sbt "++3.8.3; async-examples/run"
52
- ```
53
-
54
- The program models a small order-fulfillment pipeline: fetch a user and order,
55
- check warehouse stock, pack a shipment, and audit the steps. The structure is
56
- intentionally linear so you can read it top-to-bottom as a tutorial.
57
-
58
- ### Direct-style fulfillment
59
-
60
- The heart of the demo is straight-line code over suspending steps — no
61
- callback nesting, no manual `flatMap` chains:
62
-
63
- ```scala
64
- def fulfill(orderId: Int): Async[Shipment] = Async.async {
65
- val order = fetchOrder(orderId).await
66
- val lines = order.items.map { item =>
67
- val stock = stockFor(item.sku).await
68
- if (stock.onHand < item.qty)
69
- throw new IllegalStateException(s"short ${item.sku}")
70
- (item.sku, item.qty)
71
- }
72
- Shipment(orderId, lines, carrier = "zio-blocks-express")
55
+ `Async[A]` is the type you will spend nearly all your time with. Every way of creating a value, every way of transforming one, and every way of running one produces or consumes an `Async[A]`. If you only learn this type, you can already write complete programs — the rest of the module exists to feed values into it or to control one that is already running.
56
+
57
+ `Completer[A]` is the type you reach for next, and the reason is a problem you have almost certainly hit: some library hands you a result through a callback rather than returning it. `Async.promise` gives you a `Completer`, you pass that to the callback, and you get back an `Async[A]` that completes when the callback fires. From that point on it behaves like any other `Async[A]`, so the callback-based API disappears into ordinary code.
58
+
59
+ `Async.Running[A]` appears when you start work without waiting for it. Calling `.start` on an `Async[A]` begins the computation immediately and hands you a `Running` as a receipt. Keep it, and you can wait for the result later, run several pieces of work at once and collect them all, or stop the work early.
60
+
61
+ `Cancelable` is that last ability on its own — a single way to say "stop this." `Async.Running` provides it, and so can anything else you write that needs to be stoppable.
62
+
63
+ `Pollable[A]` is the one type most programs never touch. It is the extension point for teaching the module about a brand-new source of delayed results — a timer, a socket read, a platform-specific callback. Implementing one makes your source usable anywhere an `Async[A]` is expected. Reach for it only when you are wiring up something genuinely new; for ordinary callback bridging, `Async.promise` and a `Completer` are the right tools.
64
+
65
+ ## Evaluation Model
66
+
67
+ `Async[A]` is **eager**, not a lazy `IO`. Building one performs its synchronous work straight away — constructing the value *runs* it, up to the first point where it genuinely has to wait:
68
+
69
+ ```scala
70
+ import zio.blocks.async._
71
+
72
+ // "computing" is printed by this line, not by the one below it.
73
+ val fa: Async[Int] = Async.attempt { println("computing"); 42 }
74
+ val n: Int = fa.block // the value was already there; nothing more runs
75
+ ```
76
+
77
+ The same is true across the module. `Async.promise` runs its setup block when you call it, and `Async.succeed(x).map(f)` applies `f` immediately, because `x` is right there. Only a combinator applied to a value that is *already suspended* defers: then the function is kept and runs when a driver settles the value.
78
+
79
+ A direct-style block splits the same way, at its first genuine wait. Everything above that line runs as you build the value; everything below it is kept for later:
80
+
81
+ ```scala
82
+ Async.async {
83
+ val cfg = loadConfig() // ┐
84
+ logger.info("starting") // ├ runs now, as this value is built
85
+ val base = compute(cfg) // ┘
86
+ val row = fetchRow(base).await // the first genuine wait
87
+ transform(row) // runs later, when a driver settles it
88
+ }
89
+ ```
90
+
91
+ An `await` whose value is ready before it is asked does not count as a wait. It hands the value over on the spot and the block carries straight on, so the dividing line is not the first `await` you wrote — it is the first one that has nothing to give yet:
92
+
93
+ ```scala
94
+ Async.async {
95
+ val a = Async.succeed(1).await // already a value: no wait, keep going
96
+ val b = compute(a).await // also ready: no wait, keep going
97
+ val c = fetchFromNetwork().await // nothing yet — the block stops here
98
+ a + b + c // deferred, along with everything below
73
99
  }
74
100
  ```
75
101
 
76
- A failure from any `.await` short-circuits the block as a failed `Async`, the
77
- same as throwing inside synchronous code.
102
+ The practical consequence is that you cannot find the pause by counting `await`s. A block whose values are all ready pauses nowhere and finishes as you construct it; a block whose *first* `await` is a network call has run none of the lines below it by the time `Async.async { … }` hands you a value.
103
+
104
+ So where does waiting come from at all? From exactly one place: a [`Pollable`](#pollable) that was asked for its value and answered *not yet*. That is the only thing in the module that can make a computation pending. Everything else already has its answer — `succeed` has a value, `fail` has a cause, `attempt` has run, `map` merely applies a function.
105
+
106
+ Waiting then spreads in one direction only: to the combinators stacked on top of that pending value, which cannot produce a result until it does.
107
+
108
+ ```scala
109
+ val c = new Completer[Int] // a Pollable; not completed yet
110
+ val a = c.peek.map(_ + 1) // above a pending value → deferred
111
+ val b = a.flatMap(n => Async.succeed(n * 2)) // still above it → deferred
112
+
113
+ val d = Async.succeed(1).map(_ + 1) // no pending value anywhere → already ran
114
+ ```
115
+
116
+ `a` and `b` wait only because they sit on top of a `Completer` nobody has completed. `d` shares none of that history, so it is simply the number `2` — the `+ 1` happened as the line was evaluated.
78
117
 
79
- ### Callback bridge
118
+ One consequence catches people out. If a function you pass to `map`, `flatMap` or `tap` throws, and the value it is applied to is *ready*, that function runs as you build the value — so the exception escapes at that line rather than becoming a failed `Async` you can `catchAll`. Only `Async.attempt` turns a throw into a `Failure`; the combinators do not:
80
119
 
81
- Legacy APIs that take success/error callbacks lift cleanly through
82
- `Async.promise` (Scala 3 context-function style):
120
+ ```scala
121
+ Async.succeed(text).map(_.toInt) // throws here if text is not a number
122
+ Async.succeed(text).flatMap(s => Async.attempt(s.toInt)) // fails as an Async instead
123
+ ```
124
+
125
+ The "only one place" part is what distinguishes `Async` from a lazy effect type. `map`, `flatMap` and `zipWith` do not themselves defer anything, and building a chain does not create a plan to be executed later. If you cannot point at a value still waiting to be completed, nothing in your chain is pending: it has all already run.
126
+
127
+ Two things follow. Any pending computation can be traced to the thing it is waiting on — a `Completer` for a callback bridge, an [`Async.Running`](#asyncrunning) for started work, or your own `Pollable`. And the waiting is temporary and forward-only: when that value is completed, everything above it becomes ready, and nothing below it was ever held up, because that code had already run.
128
+
129
+ This is a deliberate trade. Eager evaluation is what keeps the ready path allocation-free — no effect tree, no per-step thunk, none of the wrapper objects most effect types build — and that is where the throughput comes from. It is also why `Async[A]` costs close to nothing on a mostly-synchronous path: when there is nothing to wait for, chaining operations onto a value just runs them.
130
+
131
+ The costs are real too. Building a value has effects, so `Async` is not referentially transparent: you cannot move a construction around, or replace a value with the expression that produced it, and be sure the program still means the same thing.
132
+
133
+ And letting go of a value does not stop it. With a lazy effect type, discarding an unused effect discards the work with it, because none of it had happened yet. Here the work is already under way:
134
+
135
+ ```scala
136
+ val running = Async.start(uploadHugeFile())
137
+
138
+ // Reassigning or forgetting `running` does not stop the upload. The worker
139
+ // keeps going and the bytes keep moving; you have only thrown away your
140
+ // ability to watch it or stop it.
141
+ ```
142
+
143
+ Stopping requires asking, through [`Cancelable#cancel`](#cancelable), and that reaches less far than you might expect.
144
+
145
+ The handle is your only route to that request. There is no registry of running computations to consult and no supervisor to ask, so a handle you have dropped cannot be recovered: that work becomes unstoppable for the rest of the process, and it finishes on its own schedule, holding its thread and its socket until it does. Nothing counts how many observers are left, so nothing notices when the last one goes away.
146
+
147
+ The rule that falls out is to decide at `start` time whether this work might ever need stopping. If it might, give the handle somewhere to live:
83
148
 
84
149
  ```scala
85
- val json: Async[String] =
86
- Async.promise[String] {
87
- // Capture the completer — nested callbacks do not inherit the `?=>` context.
88
- val completer = summon[Completer[String]]
89
- legacyHttpGet("/users/42", completer)
150
+ import java.util.concurrent.atomic.AtomicReference
151
+
152
+ class Uploader {
153
+ private val current = new AtomicReference[Async.Running[Unit]]()
154
+
155
+ def begin(): Unit = {
156
+ val previous = current.getAndSet(Async.start(uploadHugeFile()))
157
+ if (previous ne null) previous.cancel() // stop watching the one displaced
90
158
  }
159
+
160
+ def abort(): Unit = {
161
+ val running = current.getAndSet(null)
162
+ if (running ne null) running.cancel() // possible only because it was kept
163
+ }
164
+ }
91
165
  ```
92
166
 
93
- ### Custom asynchronous leaves
167
+ Note what `begin` has to do on the way past: replacing a handle means cancelling the one it displaces, or that upload becomes unstoppable while still running. The `AtomicReference` is there because the field is reachable from more than one thread — `abort` may well be called while `begin` is assigning.
94
168
 
95
- When you need a bespoke source of suspension — a socket read, a timer, a
96
- foreign runtime — implement [[Pollable]] and return it from `flatMap` to
97
- **sequence** it, or store it via `Async.succeed` / `map` to keep it as a
98
- **value** (the runtime wraps pollable success values so combinators never
99
- mistake them for suspended computations; note that the top-level drivers —
100
- `.block`, `Async.start`, and the interop converters — do drive a directly
101
- stored pollable for its effects at delivery, settling to the pollable itself).
102
- The showcase includes a `Delayed` pollable that becomes ready after a few
103
- scheduler ticks.
169
+ A [`Using`](#integration-points) block does the same job when the work is confined to a scope. And if the answer is that it never needs stopping, dropping the handle is fine — that is fire-and-forget, chosen deliberately rather than by accident.
104
170
 
105
- See
106
- [`AsyncShowcaseExample.scala`](https://github.com/zio/zio-blocks/blob/main/async-examples/src/main/scala/async/AsyncShowcaseExample.scala)
107
- for the full program.
171
+ In all of this `Async` sits beside `scala.concurrent.Future`, which is also eager, rather than beside cats-effect `IO` or ZIO.
108
172
 
109
- ## Installation
173
+ ### Depth Limits on Waiting Values
174
+
175
+ Call `map` on a value that is still waiting and you get back a wrapper: it remembers the original value and the function you gave it. Ask that wrapper for a result and it has to ask the original first, because until the original produces something there is nothing to apply the function to. That question is an ordinary method call, so the wrapper is left part-way through its own work — holding its place on the call stack — while the value underneath answers.
176
+
177
+ Stack several and each repeats the pattern. Take `c.peek.map(f).flatMap(g).map(h)`, where `c` is a `Completer` nobody has completed yet. Asking the outermost wrapper sets off a chain of questions inward, and every one of them waits where it stands:
178
+
179
+ ```
180
+ map h asks … still waiting
181
+ └─ flatMap g asks … still waiting
182
+ └─ map f asks … still waiting
183
+ └─ c answers "not yet" — and that travels back out through all three
184
+ ```
185
+
186
+ None of them can finish until the innermost one answers, so all of them are held open at the same time. A chain written N deep costs N held-open calls, every single time it is asked.
187
+
188
+ Most effect systems avoid that with a trampoline: rather than calling its child directly, each step returns a small object meaning *"do this next"* to a loop that keeps running steps until one produces a value. One loop frame serves any depth. The price is paid on every step of every poll — an object allocated to describe the step, and a dispatch through the loop instead of a direct call.
189
+
190
+ There is no third option, so the choice was between the two:
191
+
192
+ | Approach | Depth | Cost per step |
193
+ |--------------|---------------------------|----------------------------------------------------|
194
+ | Trampoline | Unlimited | An object allocated, and a dispatch, on every poll |
195
+ | Direct calls | Limited by the call stack | Nothing |
196
+
197
+ `Async` takes the second. That is why nothing is allocated while polling, and it is also why a long enough chain over a waiting value ends in a `StackOverflowError` rather than a slowdown — the ceiling is the bill for the speed, not an oversight.
198
+
199
+ You are unlikely to meet it by hand. A handful of `map` and `flatMap` calls around a network request is nowhere near the limit. It becomes a real risk when the length of the chain is decided by *data* — one `flatMap` per row, per file, per retry — because then the depth is however large the input happens to be, and code that is comfortable in a test can overflow in production on a bigger batch.
200
+
201
+ Whether you are anywhere near the limit comes down to a single question: is the chain being built on top of a value that is still waiting?
202
+
203
+ - **On a value that is already there, chains are safe at any length.** Each step runs as you write it and gives back a plain value again, so nothing is left holding anything open. A loop like `var fa = …; while (…) fa = fa.flatMap(g)` stays flat no matter how many times it goes round — the suite takes it to a million.
204
+ - **On a value that is still waiting, a long chain is not safe.** Every `fa.flatMap(g)`, `fa.map(g)` or `fa.zipWith(…)` wraps the one before it, so asking for the result opens one call per wrapper before anything can answer, and the program runs out of stack somewhere around 50,000–100,000 on a default JVM. How you wrote the loop makes no difference — a recursive `def loop(n) = src.flatMap(_ => loop(n - 1))` and an iterative `fa = fa.flatMap(_ => src.flatMap(…))` build the same stack of wrappers.
205
+ - **`Async.collectAll` and a `while` loop inside `Async.async` stay safe even when the values are still waiting** — both are tested at 50,000 such steps. `collectAll` is a single step that walks the collection itself, stacking no wrappers at all, and the direct-style loop runs one turn each time it is asked rather than building the whole chain in advance.
206
+
207
+ So for long, wait-heavy work, reach for `collectAll` or an `Async.async` `while` loop instead of a hand-built tower of `flatMap`. `Future` never runs into this, but only because it sends every `flatMap` through an `ExecutionContext`; `Async` skips that hop to stay fast and takes the depth limit instead.
208
+
209
+ ### Pending Suspensions Differ by Platform
210
+
211
+ Say a computation has hit a real wait. When the thing it waits for finally arrives, what makes the rest of the block run? Each platform answers with whatever its own runtime does fastest, so the answers differ:
212
+
213
+ - **JVM, and Scala.js on Scala 2 or Scala 3 before 3.8** — nothing runs it for you. The value sits there until something asks it for a result: `.block`, `.start`, or an interop converter. Build a value and never drive it, and the code after the wait does not run late — it never runs at all.
214
+ - **Scala.js on Scala 3.8 and later** — the block compiles into a real JavaScript async function, and JavaScript already has something whose job is resuming those: the event loop. It is always running, you did not start it, and you cannot opt out of it. So once the awaited value arrives, your code carries on by itself, whether or not anyone is watching.
215
+
216
+ ```scala
217
+ val fa = Async.async { record(fetchRow().await) }
218
+
219
+ // JVM: fetchRow may well finish, but `record` has not run — nothing drove fa.
220
+ // Scala.js 3.8+: once fetchRow finishes, `record` runs anyway.
221
+ ```
222
+
223
+ You are unlikely ever to see this. Values get built in order to be used, and the moment you `block` on one, `start` it, or hand it to a `Future`, both platforms behave identically and produce the same result. Noticing the difference takes a peculiar shape: build a block, let the thing it waits for arrive, then never drive it — a program that has already gone wrong, since it constructed work and then discarded it.
224
+
225
+ It is worth documenting because it changes *when side effects happen*. If the code after a wait prints, writes a file, or bumps a counter, Scala.js may do that without you driving anything, while the JVM will not. Cross-platform code that leans on "this has not run yet" is leaning on something true in only one of the two.
226
+
227
+ ## How They Work Together
228
+
229
+ A computation moves through four phases: construct leaf values, compose them, drive the result, then observe or cancel, as shown in the following flow diagram:
230
+
231
+ ```
232
+ ┌─ 1. CONSTRUCT — the synchronous part runs now ─────────────────┐
233
+ │ Async.succeed(a) a value you already have │
234
+ │ Async.fail(t) a failure you already have │
235
+ │ Async.attempt { … } runs the block, here, on this thread │
236
+ │ Async.promise { … } runs its setup block now │
237
+ │ new Pollable[A] { … } the one genuinely deferred leaf │
238
+ └────────────────────────────────────────────────────────────────┘
239
+ │
240
+ ▼
241
+ Async[A] ── ready, failed, or waiting on something
242
+ │
243
+ ▼
244
+ ┌─ 2. COMPOSE — runs now if ready, defers if not ────────────────┐
245
+ │ map flatMap zipWith tap │
246
+ │ catchAll ensuring collectAll │
247
+ │ │
248
+ │ on a ready value the function runs immediately; │
249
+ │ on a pending one it is kept for the driver to run │
250
+ └────────────────────────────────────────────────────────────────┘
251
+ │
252
+ ▼
253
+ ┌─ 3. DRIVE — settle whatever is still pending ──────────────────┐
254
+ │ .block .start .toFuture │
255
+ │ wait right here run in the .toJsPromise │
256
+ │ background hand it to the │
257
+ │ │ │ platform │
258
+ │ ▼ ▼ │
259
+ │ the A, or Async.Running[A] │
260
+ │ the Throwable — your receipt │
261
+ └────────────────────────────────────────────────────────────────┘
262
+ │
263
+ ▼
264
+ ┌─ 4. OBSERVE or CANCEL — using the receipt ─────────────────────┐
265
+ │ .block .flatMap .zipWith wait for it, or compose more │
266
+ │ .cancel() stop it (via Cancelable) │
267
+ └────────────────────────────────────────────────────────────────┘
268
+ ```
110
269
 
111
- Add the following to your `build.sbt`:
270
+ Three of these types are closely related, and seeing why makes the module much smaller than it first looks. `Pollable[A]` answers one question — *is the result ready yet?* — and anything that can answer it is a `Pollable`:
112
271
 
113
- ```sbt
114
- libraryDependencies += "dev.zio" %% "zio-blocks-async" % "0.0.51"
115
272
  ```
273
+ Pollable[A] — "a result that is not here yet"
274
+ │
275
+ ├── Completer[A] you complete it yourself, once, from a callback
276
+ ├── Async.Running[A] work that is already running; can also be stopped
277
+ └── Failure a computation that has already failed
278
+ ```
279
+
280
+ That shared parent is what lets all three be used interchangeably. Wherever an `Async[A]` is expected, you can supply any of them, and every combinator — `map`, `flatMap`, `zipWith`, and the rest — works on the result without knowing or caring which one it is.
281
+
282
+ `Failure` is on the list because failing is just another way of being finished — a computation that has failed is not waiting for anything. [`Failure`](#failure) describes what that means for the combinators downstream of it.
283
+
284
+ You will use `Completer` and `Async.Running` constantly, and `Failure` mostly without naming it. Writing your own `Pollable` is the rare case, reserved for teaching the module about a new source of delayed results.
285
+
286
+ Two details the diagram leaves out. Composing over a ready value is allocation-free, while composing over a waiting one allocates a `Pollable` that the driver walks poll by poll. And the handle from phase 3 is itself an `Async`, which is what makes phase 4 ordinary composition rather than a separate API.
287
+
288
+ The following snippet grounds all four phases in an example: two off-thread `Completer` completions are composed with `zipWith` and driven by `block`:
289
+
290
+ ```scala
291
+ import zio.blocks.async._
292
+
293
+ def delayed[A](value: A, ms: Long): Async[A] = {
294
+ val c = new Completer[A]
295
+ val t = new Thread(new Runnable {
296
+ def run(): Unit = { Thread.sleep(ms); c.succeed(value) }
297
+ })
298
+ t.setDaemon(true)
299
+ t.start()
300
+ c.peek // the Completer is itself an Async[A]
301
+ }
302
+
303
+ // Phase 1 and 2: construct two off-thread leaves and compose with zipWith
304
+ val r: Async[Int] = delayed(3, 30).zipWith(delayed(4, 5))(_ + _)
305
+
306
+ // Phase 3: drive — parks the calling thread until both off-thread wakers fire
307
+ val result: Int = r.block // => 7
308
+ ```
309
+
310
+ A second example shows what `Async.collectAll` guarantees: the results come back in the order you listed the computations, not the order they happened to finish. To make that visible, the delays below are deliberately reversed — the first element takes the longest, the last finishes almost immediately:
311
+
312
+ ```scala
313
+ import zio.blocks.async._
314
+
315
+ // Completes with `value` after `ms`, on another thread.
316
+ def delayed[A](value: A, ms: Long): Async[A] = {
317
+ val c = new Completer[A]
318
+ val t = new Thread(new Runnable {
319
+ def run(): Unit = { Thread.sleep(ms); c.succeed(value) }
320
+ })
321
+ t.setDaemon(true)
322
+ t.start()
323
+ c.peek
324
+ }
116
325
 
117
- For cross-platform projects (Scala.js):
326
+ val ordered: Async[List[Int]] = Async.collectAll(List[Async[Int]](
327
+ delayed(1, 90), // finishes third
328
+ delayed(2, 45), // finishes second
329
+ delayed(3, 5) // finishes first
330
+ ))
118
331
 
119
- ```sbt
120
- libraryDependencies += "dev.zio" %%% "zio-blocks-async" % "0.0.51"
332
+ // Completion order is 3, 2, 1 — the list is still 1, 2, 3.
333
+ val results: List[Int] = ordered.block // => List(1, 2, 3)
121
334
  ```
122
335
 
123
- Supported platforms: JVM and Scala.js, Scala 2.13 and Scala 3.x. The
124
- direct-style `Async.async` block is rewritten by dotty-cps-async on Scala 3
125
- (JVM and older Scala 3 JS), by a hybrid backend on Scala 3.8+ JS (native
126
- `js.async` / `js.await` for direct-position awaits, with the dotty-cps-async
127
- transform as fallback for awaits inside closures, by-name arguments, or nested
128
- methods), and by a hand-written macro on Scala 2.
336
+ Without that guarantee you would have to tag each computation and re-sort the results yourself. Because `collectAll` keeps the positions, you can zip the output against the input list — or pattern-match on it positionally — and trust that element *n* belongs to computation *n*.
129
337
 
130
- ## Constructors
338
+ ## Operations
131
339
 
132
- `Async.succeed` lifts a pure value; `Async.fail` lifts an error; `Async.attempt`
133
- catches a thrown exception and turns it into a failure.
340
+ Everything you can do with an `Async[A]`: make one, transform it, combine it with another, recover from a failure, and eventually run it.
341
+
342
+ One property is worth carrying into the signatures below. `Async[A]` is declared `Async[+A]`, which makes it *covariant*: whenever `B` is a subtype of `A`, an `Async[B]` counts as an `Async[A]`. That matters because `Async.fail` and `Async.never` have no value to offer, so their type is `Async[Nothing]` — and `Nothing` is a subtype of every type in Scala. An `Async[Nothing]` is therefore an `Async[String]`, an `Async[User]`, an `Async` of anything at all:
134
343
 
135
344
  ```scala
136
345
  import zio.blocks.async._
137
346
 
138
- val ready: Async[Int] = Async.succeed(42)
139
- // ready: Async[Int] = 42
347
+ case class User(id: Int, name: String)
348
+
349
+ def fetchUser(id: Int): Async[User] =
350
+ if (id < 0) Async.fail(new IllegalArgumentException("bad id")) // Async[Nothing]
351
+ else Async.succeed(User(id, "sam")) // Async[User]
352
+
353
+ val forever: Async[User] = Async.never // Async[Nothing] fits too
354
+ ```
355
+
356
+ Without covariance neither of those would compile against the declared `Async[User]`, and you would be writing `Async.fail[User](…)` or a cast at every failure. This is a property you notice only through the errors it saves you from.
140
357
 
141
- val failed: Async[Nothing] = Async.fail(new RuntimeException("boom"))
142
- // failed: Async[Nothing] = zio.blocks.async.Failure@12260721
358
+ ### Creating Values
143
359
 
144
- val caught: Async[Int] = Async.attempt(Integer.parseInt("123"))
145
- // caught: Async[Int] = 123
360
+ The companion object provides factories for constructing leaf `Async[A]` values:
361
+
362
+ ```scala
363
+ object Async {
364
+ def succeed[A](a: A): Async[A]
365
+ def fail(cause: Throwable): Async[Nothing]
366
+ def attempt[A](body: => A): Async[A]
367
+ def promise[A](body: Completer[A] => Unit): Async[A] // shape differs on Scala 3; see Completer
368
+ def start[A](body: => A): Async.Running[A]
369
+ val never: Async[Nothing]
370
+ def collectAll[A](as: IterableOnce[Async[A]]): Async[List[A]]
371
+ def async[A](body: A): Async[A] // rewritten in place: a macro on Scala 2,
372
+ // a transparent inline def on Scala 3.
373
+ // See the Direct Style pattern
374
+
375
+ // JVM only
376
+ def fromFuture[A](future: scala.concurrent.Future[A]): Async[A]
377
+ def fromCompletionStage[A](cs: java.util.concurrent.CompletionStage[A]): Async[A]
378
+ }
146
379
  ```
147
380
 
148
- `Async.collectAll` sequences a collection of `Async` values, short-circuiting on
149
- the first failure:
381
+ `Async.succeed` lifts a pure, immediately-available value into an `Async[A]`:
150
382
 
151
383
  ```scala
152
- val all: Async[List[Int]] =
153
- Async.collectAll(List(Async.succeed(1), Async.succeed(2), Async.succeed(3)))
154
- // all: Async[List[Int]] = List(1, 2, 3)
384
+ import zio.blocks.async._
385
+
386
+ val ready: Async[Int] = Async.succeed(42)
387
+ val result: Int = ready.block // => 42
155
388
  ```
156
389
 
157
- ## Evaluation model: eager up to suspension
390
+ `Async.fail` creates a terminal failure; [`Failure`](#failure) covers how it short-circuits the rest of a chain:
158
391
 
159
- `Async` is **eager**, not a lazy `IO`. Constructing an `Async` performs all of
160
- its synchronous work immediately — building the value *runs* it, up to the first
161
- point where it genuinely has to wait:
392
+ ```scala
393
+ import zio.blocks.async._
394
+
395
+ val boom: Async[Int] = Async.fail(new RuntimeException("boom"))
396
+ val result: Int = boom.catchAll(_ => Async.succeed(-1)).block // => -1
397
+ ```
162
398
 
163
- - `Async.attempt(body)` runs `body` now (on the calling thread); `Async.promise`
164
- runs its setup block now; `Async.async { ... }` runs its synchronous prefix
165
- (and any **ready** `.await`s) now; `succeed(x).map(f)` runs `f` now. Only a
166
- combinator applied to an **already-suspended** value defers — its function
167
- runs when the value is later driven.
168
- - The single genuinely-lazy primitive is a custom [`Pollable`](#low-level-building-blocks-pollable):
169
- its `poll` runs only when a driver asks for the value. Suspension exists only
170
- *downstream of* an unresolved `poll`.
399
+ `Async.attempt` captures a by-name expression and converts any thrown `Throwable` into a failure:
171
400
 
172
- This makes the success/ready path allocation-free (no effect tree, no per-step
173
- thunk) — the source of its throughput — at the cost of referential transparency
174
- (building has effects) and cancel-by-drop (use [`Cancelable.cancel`](#eager-cancellable-running-asyncstart-and-asyncrunning)
175
- instead). It sits next to `scala.concurrent.Future` (also eager) rather than
176
- cats-effect `IO` / ZIO (lazy).
401
+ ```scala
402
+ import zio.blocks.async._
403
+
404
+ val parsed: Async[Int] = Async.attempt("42".toInt)
405
+ val bad: Async[Int] = Async.attempt("nope".toInt) // => Async.fail(NumberFormatException)
406
+ val result: Int = bad.catchAll(_ => Async.succeed(0)).block // => 0
407
+ ```
177
408
 
178
- ### What happens at a pending suspension differs by platform — by design
409
+ `Async.never` is a permanently-suspended `Async[Nothing]` — a placeholder where an `Async[A]` is required but no value should ever arrive, and the usual way to test cancellation, as shown under [`Async.Running`](#asyncrunning).
179
410
 
180
- Once an `Async` hits a genuinely **pending** suspension (an await of a
181
- not-yet-complete value), what advances it follows each platform's *fastest*
182
- suspension mechanism, so the two platforms diverge:
411
+ `Async.start(body: => A)` runs a body on a background worker and hands back an `Async.Running[A]`; [Concurrent Fan-Out](#concurrent-fan-out-via-running) covers when to reach for it and the trap to avoid:
183
412
 
184
- - **JVM (and Scala.js on Scala 3 < 3.8, and Scala 2):** the value is a
185
- poll-driven `Pollable` with no ambient driver. The continuation after the
186
- pending suspension runs only when an external driver polls it — `.block`,
187
- `fa.start`, or an interop runner (`toFuture` / `unsafeRunAsync`). A built-but-
188
- never-driven block leaves that continuation un-run.
189
- - **Scala.js on Scala 3.8+:** `Async.async`/`.await` compile to native
190
- `js.async` / `js.await` — a real JavaScript async function whose driver *is*
191
- the event loop. Once the awaited value settles, the continuation self-resumes
192
- off the microtask queue even if nothing polls the `Async`. This is the same
193
- event-loop driving that makes await-heavy direct-style blocks substantially
194
- faster than the dotty-cps-async backend, so the behavior is intentional, not a
195
- defect: it is the zero-cost default of the fastest JS suspension primitive.
413
+ ```scala
414
+ import zio.blocks.async._
196
415
 
197
- In practice this is invisible — you always drive an `Async` you build — and the
198
- *value* is identical on every cell. The divergence is observable only by a block
199
- that is constructed, has its awaited value settle, and is then never driven.
416
+ val running: Async.Running[Int] = Async.start { 42 }
417
+ val result: Int = running.block // => 42
418
+ ```
200
419
 
201
- ## Transforming values
420
+ ### Transformation
202
421
 
203
- On the ready path the transformers apply your function directly to the
204
- underlying value; only a genuinely pending `Async` takes the suspended slow
205
- path.
422
+ Pure transformations apply a function to the success value and return a new `Async`:
206
423
 
207
424
  ```scala
208
- val mapped: Async[Int] = Async.succeed(20).map(_ + 1)
209
- // mapped: Async[Int] = 21
425
+ implicit class AsyncOps[A](fa: Async[A]) {
426
+ def map[B](f: A => B): Async[B]
427
+ def flatMap[B](f: A => Async[B]): Async[B]
428
+ def as[B](b: B): Async[B]
429
+ def unit: Async[Unit]
430
+ }
431
+
432
+ // flatten is a separate extension, on a nested Async:
433
+ implicit class AsyncNestedOps[A](ffa: Async[Async[A]]) {
434
+ def flatten: Async[A] // collapses one nesting level
435
+ }
436
+ ```
437
+
438
+ `map` applies a pure function and `flatMap` sequences a dependent second computation:
210
439
 
211
- val chained: Async[Int] = Async.succeed(20).flatMap(n => Async.succeed(n * 2))
212
- // chained: Async[Int] = 40
440
+ ```scala
441
+ import zio.blocks.async._
213
442
 
214
- val recovered: Async[Int] =
215
- Async.fail(new RuntimeException("nope")).catchAll(_ => Async.succeed(-1))
216
- // recovered: Async[Int] = -1
443
+ val result: Async[String] =
444
+ Async.succeed(21)
445
+ .map(_ * 2)
446
+ .flatMap(n => Async.succeed(s"value: $n"))
447
+ val out: String = result.block // => "value: 42"
217
448
  ```
218
449
 
219
- ### Combining with `zip`
450
+ ### Composition
451
+
452
+ Compositional operators combine independent or dependent `Async` values:
453
+
454
+ ```scala
455
+ implicit class AsyncOps[A](fa: Async[A]) {
456
+ def zipWith[B, C](that: Async[B])(f: (A, B) => C): Async[C]
457
+ def zip[B](that: Async[B])(implicit t: Tuples[A, B]): Async[t.Out] // flattens; see below
458
+ def tap(f: A => Async[Any]): Async[A]
459
+ def ensuring(finalizer: Async[Any]): Async[A]
460
+ def *>[B](that: Async[B]): Async[B]
461
+ def <*[B](that: Async[B]): Async[A]
462
+ def orElse[B](that: => Async[B]): Async[_] // result type merges A and B via Concat typeclass
463
+ }
464
+ ```
220
465
 
221
- `zip` fuses two `Async` values into a tuple using the `combinators` module's
222
- `Tuples` combiner, so chained zips flatten automatically (`a zip b zip c`
223
- yields `Async[(A, B, C)]`, not `Async[((A, B), C)]`). Use `zipWith` to combine
224
- with an explicit function:
466
+ `zipWith` waits for both sides and combines their results; `tap` runs a side-effecting action while passing the original value through. `zip` pairs the two results, and chains of it stay flat rather than nesting — `a zip b zip c` yields `Async[(A, B, C)]`, not `Async[((A, B), C)]` — because it combines through the `Tuples` instances described in the [combinators reference](combinators.md):
225
467
 
226
468
  ```scala
227
- val zipped: Async[(Int, String)] =
228
- Async.succeed(1).zip(Async.succeed("two"))
229
- // zipped: Async[Tuple2[Int, String]] = (1, "two")
469
+ import zio.blocks.async._
230
470
 
231
- val summed: Async[Int] =
471
+ val combined: Async[Int] =
232
472
  Async.succeed(3).zipWith(Async.succeed(4))(_ + _)
233
- // summed: Async[Int] = 7
473
+ val tapped: Async[Int] =
474
+ combined.tap(v => Async.attempt(println(s"sum is $v")))
475
+ val result: Int = tapped.block // => 7; "sum is 7" was printed by the line above
476
+ ```
477
+
478
+ `*>` and `<*` sequence two effects and discard the left or right result respectively:
479
+
480
+ ```scala
481
+ import zio.blocks.async._
482
+
483
+ val logged: Async[Int] =
484
+ Async.attempt(println("starting")).*>(Async.succeed(42))
485
+ val result: Int = logged.block // => 42
486
+ ```
487
+
488
+ ### Error Handling
489
+
490
+ `Async` represents failure as a `Throwable` and provides dedicated recovery operators:
491
+
492
+ ```scala
493
+ implicit class AsyncOps[A](fa: Async[A]) {
494
+ def catchAll[A1 >: A](f: Throwable => Async[A1]): Async[A1]
495
+ def mapError(f: Throwable => Throwable): Async[A]
496
+ def foldCause[B](onFailure: Throwable => B)(onSuccess: A => B): Async[B]
497
+ def either: Async[Either[Throwable, A]]
498
+ }
234
499
  ```
235
500
 
236
- ### Error handling
501
+ `catchAll` recovers from any failure by supplying a replacement `Async[A]`; `either` converts the outcome to an `Either` so the failure surface is visible in the return type:
502
+
503
+ ```scala
504
+ import zio.blocks.async._
505
+
506
+ val safe: Async[Either[Throwable, Int]] =
507
+ Async.fail(new Exception("oops")).either
508
+ val result: Either[Throwable, Int] = safe.block // => Left(Exception("oops"))
509
+ ```
237
510
 
238
- `catchAll` recovers a failure, `mapError` transforms the cause, `orElse`
239
- falls back to another `Async`, and `either` reifies the outcome:
511
+ `foldCause` handles both the success and failure branches in a single call without allocating a recovery `Async`:
240
512
 
241
513
  ```scala
242
- val asEither: Async[Either[Throwable, Int]] =
243
- Async.fail(new RuntimeException("x")).either
244
- // asEither: Async[Either[Throwable, Int]] = Left(
245
- // java.lang.RuntimeException: x
246
- // )
514
+ import zio.blocks.async._
247
515
 
248
- val fallback: Async[Int] =
249
- Async.fail(new RuntimeException("x")).orElse(Async.succeed(0))
250
- // fallback: Async[Int] = 0
516
+ val message: Async[String] =
517
+ Async.attempt("42".toInt).foldCause(
518
+ (err: Throwable) => s"failed: ${err.getMessage}"
519
+ )(
520
+ (n: Int) => s"parsed: $n"
521
+ )
522
+ val result: String = message.block // => "parsed: 42"
251
523
  ```
252
524
 
253
- ### Conditional effects
525
+ ### Driving
254
526
 
255
- `when` / `unless` run an `Async` only when a condition holds, discarding its
256
- value. `Async.never` is an `Async` that never completes — useful as a sentinel:
527
+ Driving settles whatever part of an `Async[A]` is still waiting, and delivers the result through one of three mechanisms:
257
528
 
258
529
  ```scala
259
- val maybe: Async[Unit] = when(1 < 2)(Async.succeed(()))
260
- // maybe: Async[Unit] = ()
530
+ implicit class AsyncOps[A](fa: Async[A]) {
531
+ def block: A // parks calling thread; re-throws on failure
532
+ def await: A // inside Async.async { } only; the rewrite
533
+ // removes it. Using it elsewhere does not compile
534
+ def start: Async.Running[A]
535
+ def toFuture(implicit ec: scala.concurrent.ExecutionContext): scala.concurrent.Future[A]
536
+ def toCompletableFuture(implicit ec: scala.concurrent.ExecutionContext)
537
+ : java.util.concurrent.CompletableFuture[A] // JVM only
538
+ }
539
+ ```
261
540
 
262
- val skipped: Async[Unit] = unless(1 < 2)(Async.succeed(()))
263
- // skipped: Async[Unit] = ()
541
+ `block` parks the calling thread until the computation settles, then returns the value or re-throws the underlying `Throwable`:
264
542
 
265
- val forever: Async[Nothing] = Async.never
266
- // forever: Async[Nothing] = zio.blocks.async.Async$$anon$1@65db325f
543
+ ```scala
544
+ import zio.blocks.async._
545
+
546
+ val result: Int = Async.succeed(42).map(_ + 1).block // => 43
267
547
  ```
268
548
 
269
- ## Direct style: `Async.async` and `.await`
549
+ Two limits apply. On the JVM, never call `block` from inside a `poll` — you would be putting to sleep the very thread that has to deliver your result, which deadlocks the loop. Keep it at the edge of your program: `main`, a test, the boundary with synchronous code.
550
+
551
+ On Scala.js there is no thread to park at all. A ready value returns as usual, but a *pending* one gets a single chance to complete synchronously, and if it has not, `block` throws `IllegalStateException`. Scala.js code should reach for `toFuture` or `toJsPromise` and let the event loop deliver the result, or stay inside `Async.async { … }` and use `await`.
270
552
 
271
- Inside an `Async.async { ... }` block you can write straight-line code and use
272
- `.await` to extract the value of any `Async`. The block is rewritten at compile
273
- time into a non-blocking `flatMap` / `map` chain — there is no thread blocking
274
- on the happy path. `.await` is **lexically restricted** to `Async.async`
275
- blocks; using it elsewhere is a compile error.
553
+ `start` hands whatever is still waiting to a *driver* and returns an `Async.Running[A]` immediately, without blocking. The driver is the thing that keeps asking a pending value for its result. On the JVM it is a chain of serialized tasks on `ForkJoinPool.commonPool()`; on Scala.js it is the microtask queue. Neither holds a thread for the duration: a task exits as soon as its current pollable is still pending, and the waker that `poll` registered submits the next task when there is something new to see.
554
+
555
+ `Async.start(body)` is the other entry point and it is not the same mechanism. It takes a block of ordinary synchronous code rather than an `Async`, so it needs somewhere to run that block: on the JVM a dedicated daemon thread named `zio-blocks-async-eval`, and on Scala.js the next microtask.
276
556
 
277
557
  ```scala
278
- def loadUser(id: Int): Async[String] = Async.succeed(s"user-$id")
279
- def loadOrders(user: String): Async[List[String]] = Async.succeed(List(s"$user-order"))
558
+ import zio.blocks.async._
280
559
 
281
- val program: Async[Int] =
282
- Async.async {
283
- val user = loadUser(1).await
284
- val orders = loadOrders(user).await
285
- orders.size
286
- }
560
+ def compute(): Int = 42
561
+
562
+ val running: Async.Running[Int] = Async.start { compute() }
563
+ val result: Int = running.block // wait here for the result
287
564
  ```
288
565
 
289
- A failure encountered by `.await` short-circuits the block and surfaces as a
290
- failed `Async`, exactly as if you had thrown — `Async.async { Async.fail(t).await }`
291
- is equivalent to `Async.fail(t)`.
292
-
293
- `.await` is also supported inside the higher-order-function closures of the
294
- standard strict collections — `List`, `Option`, `Vector`, immutable `Set`,
295
- immutable `Map`, `Array`, immutable `Queue`, and immutable `ArraySeq` — across a
296
- broad set of methods (`map` / `foreach` / `flatMap`, the predicate scans
297
- `find` / `exists` / `forall` / `filter` / `filterNot`, the folds
298
- `foldLeft` / `foldRight` / `reduce` / `reduceLeft`, the prefix scans
299
- `takeWhile` / `dropWhile`, and `collect`). Each is detailed below, with semantics
300
- that match the method's natural meaning (and the Scala 3 backends exactly). A few
301
- positions diverge between Scala 2 and Scala 3 — those are called out explicitly as
302
- **Divergence** notes:
303
-
304
- - **`List.map`** is **eager**: strict `map` applies the closure to every element
305
- first — running all construction-time side effects — producing a
306
- `List[Async[B]]`, and the awaits are then sequenced left-to-right via
307
- `Async.collectAll` (fail-fast on the first failure). This mirrors how
308
- `Array.map(async ...)` composes in JavaScript.
309
- - **`List.foreach`** is **lazy / sequential**: the closure for element `n+1` runs
310
- only after element `n`'s `.await` completes successfully, and a failed await
311
- short-circuits the remaining elements. The result is `Unit`.
312
- - **`List.flatMap`** is **lazy / sequential** like `foreach`, but accumulates each
313
- closure's `IterableOnce` into the result `List`.
314
- - **`Option.map` / `Option.flatMap` / `Option.foreach`**: an `Option` holds at
315
- most one element, so the eager/lazy distinction collapses to a single
316
- `Some`/`None` branch — `None` short-circuits (the closure never runs), `Some(x)`
317
- runs the closure and (for `map`/`flatMap`) rewraps the result; a failed await
318
- propagates.
319
- - **`Vector` / immutable `Set` / immutable `Queue` / immutable `ArraySeq`**
320
- (`map` / `flatMap` / `foreach`, plus the builder-backed methods below): **lazy
321
- / sequential** like `List.foreach` (the closure for element `n+1` runs only
322
- after element `n`'s await completes; a failure short-circuits the rest). Note
323
- that `Vector.map` / `Queue.map` / `ArraySeq.map` are lazy — only `List.map` is
324
- eager (it is the special case backed by dotty-cps-async's `ListAsyncShift`).
325
- The result **collection type is preserved** (`Vector.map` → `Vector`,
326
- `Queue.map` → `Queue`, `ArraySeq.map` → `ArraySeq`, `Set.map` → `Set`); for
327
- `Set`, the *awaited* values are deduplicated.
328
- - **`Array`** (`map` / `flatMap` / `foreach` / `filter` / `takeWhile` /
329
- `dropWhile` / `foldLeft` / `collect` / `find` / `exists` / `forall`): the
330
- result is always an `Array[B]` with the **element type preserved, including
331
- primitives** (e.g. `Array[Int].map(_.toLong)` → a primitive `Array[Long]`).
332
- `Array.map` is **eager** like `List.map` (a failing await still runs every
333
- preceding closure); `Array.flatMap` and the rest are **lazy / sequential**.
334
- The result-building HOFs (`map` / `flatMap` / `filter` / `takeWhile` /
335
- `collect`) rebuild via `Array.newBuilder`, which needs a `ClassTag[B]` — the
336
- same one the user's own `Array` HOF already required, so it resolves for any
337
- concrete element type (an abstract/path-dependent `B` without a `ClassTag` in
338
- scope is not supported, exactly as the standard-library call would not be).
339
- - **immutable `Map`** (`map` / `flatMap` / `foreach`): **lazy / sequential** over
340
- the map's `(K, V)` entries. A pair-returning `map`/`flatMap` rebuilds a
341
- `Map[K2, V2]` (later entries with the same key win); a non-pair `map`/`flatMap`
342
- widens the result to an `Iterable`, matching the standard library's overload
343
- choice. `foreach` runs the closure for each entry, returning `Unit`.
344
- - **Short-circuiting predicate scans** (`find` / `exists` / `forall`, predicate
345
- `A => Boolean`): **lazy / sequential** over any whitelisted receiver — the
346
- predicate for element `n+1` runs only after element `n`'s await completes, and
347
- the scan stops at the first decisive element (`exists` → first `true`; `forall`
348
- → first `false`; `find` → first matching element as `Some`, else `None`).
349
- `Option.find` is covered on every cell too — on Scala 2 it resolves via the
350
- `Option`→`Iterable` implicit conversion, which the macro recognizes specifically
351
- for `find`.
352
- - **`foldLeft`** (op `(B, A) => B`): **lazy / sequential** over any whitelisted
353
- receiver via `.iterator` — a left fold is inherently sequential (element
354
- `n+1`'s op needs `n`'s accumulator), so the op for element `n+1` runs only
355
- after element `n`'s await completes, and a failed await short-circuits the
356
- rest. The accumulator is threaded through and `foldLeft[B]` returns `B`
357
- directly (it may differ from the element type), so awaits in the initial
358
- accumulator are sequenced before the fold.
359
- - **`reduce` / `reduceLeft`** (op `(B, A) => B`): **lazy / sequential** over any
360
- whitelisted receiver via `.iterator` — `foldLeft` seeded by the FIRST element
361
- instead of an initial value, so the op for element `n+1` runs only after
362
- element `n`'s await completes and a failed await short-circuits the rest. A
363
- single-element receiver returns that element without running the op; an EMPTY
364
- receiver fails with `UnsupportedOperationException` (catchable via `catchAll`,
365
- rethrown by `.block`).
366
- - **`foldRight`** (op `(A, B) => B`): **lazy / sequential** but
367
- **right-associative** — `op(x1, op(x2, ..., op(xn, z)))` — so the op for the
368
- RIGHTMOST element runs first (the receiver is materialized and drained in
369
- reverse to keep the await-ordering correct). An empty receiver yields the
370
- initial accumulator (the op never runs); a failed await short-circuits the
371
- remaining (right-to-left) elements.
372
- - **`filter` / `filterNot`** (predicate `A => Boolean`): **lazy / sequential**
373
- over a `List` / `Vector` / `Array` / immutable `Set` / immutable `Queue` /
374
- immutable `ArraySeq` / `Option` — the predicate for element `n+1` runs only
375
- after element `n`'s await completes, and a failed await short-circuits the
376
- rest. The result **collection type is preserved** (`filter` keeps elements
377
- whose predicate is `true`, `filterNot` those whose predicate is `false`).
378
- **Divergence:** `Map.filter` / `Map.filterNot` with `.await` is a
379
- **Scala-2-only superset** — dotty-cps-async has no working `MapOpsAsyncShift.filter`
380
- and rejects it on Scala 3.
381
- - **`takeWhile` / `dropWhile`** (predicate `A => Boolean`): **lazy / sequential**
382
- over an ordered receiver (`List` / `Vector` / immutable `Queue` / immutable
383
- `ArraySeq` / `Array`) — these are **prefix-ordered**, so the predicate for
384
- element `n+1` runs only after element `n`'s await completes, and the FIRST
385
- element whose predicate is `false` decides the boundary (`takeWhile` keeps the
386
- leading run and discards it and the rest; `dropWhile` drops the leading run and
387
- keeps it and the rest **unconditionally**, never re-evaluating the predicate).
388
- A failed await short-circuits the rest. The result **collection type is
389
- preserved**. They are restricted to ordered receivers because a leading-prefix
390
- predicate is ill-defined on an unordered `Set` / `Map` (and `Option` does not
391
- provide them); the Scala 2 macro rejects those with an actionable compile
392
- error.
393
- - **`collect`** (partial function `{ case ... }`): **lazy / sequential** over a
394
- `List` / `Vector` / `Array` / immutable `Set` / immutable `Queue` / immutable
395
- `ArraySeq` — keeps the elements the partial function is defined at, mapping
396
- each through its (awaiting) case body; the case for element `n+1` runs only
397
- after element `n`'s await completes, and a failed await short-circuits the
398
- rest. The result **collection type is preserved**. An `Option` receiver is
399
- supported too: `None` short-circuits without evaluating the partial function,
400
- `Some(a)` yields `Some(b)` if a case matches, else `None`. A **non-pair
401
- `Map.collect`** (whose case bodies yield a `B`, so the result is an
402
- `Iterable[B]`) is supported on every cell. The case guard runs exactly once per
403
- element (Scala 2). A `.await` in a case GUARD is rejected.
404
- **Divergence:** a **pair-yielding `Map.collect`** (whose case bodies yield
405
- `(K2, V2)` pairs, so the result is a `Map[K2, V2]`) is **unsupported on every
406
- cell** — dotty-cps-async has only an `IterableOpsAsyncShift.collect[F, B]`
407
- shift (no Map-specific one), so the `Map`-returning overload is a compile error
408
- on Scala 3, and the Scala 2 macro rejects it to stay at parity. Rewrite it as
409
- `m.toVector.collect { case ... => k -> v.await }.toMap`.
410
-
411
- These behave identically across Scala 2/3 and JVM/JS **except** for the handful of
412
- positions flagged **Divergence** above (`Map.filter` / `filterNot` is a
413
- Scala-2-only superset; a pair-yielding `Map.collect` is unsupported everywhere).
414
- Because Scala desugars
415
- for-comprehensions over a `List` / `Option` / `Vector` / `Set` / `Map` into these
416
- methods,
417
- single- and multi-generator `for` comprehensions with `.await` work too
418
- (`for ... yield` → `map`; nested generators → `flatMap`/`map`; `for { ... }`
419
- without `yield` → `foreach`; a guard `if` → `withFilter`):
420
-
421
- ```scala
422
- val pairs: Async[List[Int]] = Async.async {
423
- for {
424
- i <- List(1, 2)
425
- j <- List(10, 20)
426
- } yield Async.succeed(i + j).await
427
- } // List(11, 21, 12, 22)
428
- ```
429
-
430
- > **Scala 2 limitation (current):** the Scala 2 macro supports `.await` in
431
- > sequential statements, `if` / `match` / `while` / `try`-`catch`-`finally`,
432
- > `throw`, assignments, `List` / `Option` / `Vector` / `Array` / immutable `Set` /
433
- > immutable `Queue` / immutable `ArraySeq` / immutable
434
- > `Map` `map` / `foreach` / `flatMap` closures, the short-circuiting predicate
435
- > scans `find` / `exists` / `forall`, `filter` / `filterNot`, `foldLeft`,
436
- > `foldRight`, and `reduce` / `reduceLeft` over
437
- > those receivers, the prefix-ordered `takeWhile` / `dropWhile` over ordered
438
- > receivers (`List` / `Vector` / immutable `Queue` / immutable `ArraySeq` /
439
- > `Array`), `collect` over builder-backed receivers (`List` / `Vector` / `Array`
440
- > / immutable `Set` / immutable `Queue` / immutable `ArraySeq`), and the
441
- > for-comprehensions that desugar to the former (including guards), but **rejects**
442
- > `.await` inside other function
443
- > literals / higher-order-function arguments (and HOFs over collections other than
444
- > those whitelisted families), with an actionable compile error. Those positions
445
- > are supported on Scala 3. The whitelisted set above is the **final, stable**
446
- > Scala 2 contract for the standard strict collections; positions outside it are
447
- > intentionally unsupported on Scala 2 (a custom collection, or `.await` inside an
448
- > arbitrary user lambda passed to a third-party HOF, cannot be rewritten without a
449
- > shift typeclass the Scala 2 macro deliberately does not depend on).
450
- >
451
- > Conversely, the Scala 2 macro is a strict superset for some guard shapes that
452
- > dotty-cps-async on Scala 3 currently rejects: *multiple* `List`
453
- > for-comprehension guards (chained `withFilter`), and *any* `Option`
454
- > for-comprehension guard (DCA has no `AsyncShift[Option#WithFilter]`). Single
455
- > `List` guards behave identically on every cell.
456
-
457
- ## The callback bridge: `Async.promise`
458
-
459
- `Async.promise` builds an `Async` from a callback-style API. You receive a
460
- `Completer` and call `succeed` / `fail` when the result arrives. Completion is
461
- one-shot — the first `succeed` or `fail` wins; later calls are silent no-ops. If
462
- the body completes the completer synchronously, the result collapses to a bare
463
- value with no `Pollable` allocation.
464
-
465
- On Scala 3 the completer is supplied via a context function, so you can call the
466
- top-level `succeed` / `fail` helpers directly:
467
-
468
- ```scala
469
- import zio.blocks.async._
470
-
471
- val fromCallback: Async[Int] =
472
- Async.promise[Int] {
473
- // register a callback with some external system, then:
474
- succeed(42)
475
- }
566
+ `toFuture` hands off to a `scala.concurrent.Future`, bridging into any code that already expects the standard-library async type:
567
+
568
+ ```scala
569
+ import zio.blocks.async._
570
+ import scala.concurrent.ExecutionContext.Implicits.global
571
+
572
+ val future: scala.concurrent.Future[Int] =
573
+ Async.succeed(99).toFuture
574
+ ```
575
+
576
+ ### Conditional Execution
577
+
578
+ `when` and `unless` are package-level functions (brought in by `import zio.blocks.async._`) that conditionally evaluate an `Async[Any]` based on a `Boolean` condition. The unevaluated branch is passed by name so no `Async` is constructed when the condition is false:
579
+
580
+ ```scala
581
+ import zio.blocks.async._
582
+
583
+ val flag = true
584
+ val logged: Async[Unit] = when(flag)(Async.attempt(println("running")))
585
+ val skipped: Async[Unit] = unless(flag)(Async.attempt(println("skipped")))
476
586
  ```
477
587
 
478
- On Scala 2 the body receives the `Completer` explicitly; mark it `implicit` to
479
- use the same top-level `succeed` / `fail` helpers, or call its methods directly:
588
+ ## Common Patterns
589
+
590
+ The five patterns below address the most frequent tasks: bridging callbacks, writing sequential-looking code, guaranteeing cleanup, sharing in-flight computations, and collecting parallel results.
591
+
592
+ ### Callback Bridge
593
+
594
+ Callback-based APIs all share one shape. Instead of returning the result, they return `Unit` immediately and call one of two functions you hand them once the work is done:
480
595
 
481
596
  ```scala
482
- // Scala 2
483
- Async.promise[Int] { implicit c => succeed(42) }
484
- Async.promise[Int] { c => c.succeed(42) }
597
+ // The API you are stuck with. A real one calls back later, from
598
+ // another thread; the shape is what matters here.
599
+ def legacyApi(onSuccess: String => Unit, onError: Throwable => Unit): Unit =
600
+ onSuccess("done")
485
601
  ```
486
602
 
487
- ## Running an `Async`
603
+ That signature is the problem. Because `legacyApi` returns `Unit`, there is no value to return from your own function, nothing to pass to another function, and no way to say "do this, then that" — the result only ever appears inside a callback body, so the rest of your program has to be written in there too.
604
+
605
+ `Async.promise` inverts it. It gives you a `Completer[A]`, which is a value that can be completed later, and hands you back an `Async[A]` representing the eventual result. You pass the completer's two methods where `legacyApi` expects its two functions. The body is written slightly differently on each Scala version — `c =>` on Scala 2, `c ?=>` on Scala 3, for the reason explained under [`Completer`](#completer):
606
+
607
+ <Tabs groupId="scala-version" defaultValue="scala2">
608
+ <TabItem value="scala2" label="Scala 2">
609
+
610
+ ```scala
611
+ import zio.blocks.async._
612
+
613
+ def legacyApi(onSuccess: String => Unit, onError: Throwable => Unit): Unit =
614
+ onSuccess("done")
615
+
616
+ val async: Async[String] = Async.promise[String] { c =>
617
+ legacyApi(
618
+ result => c.succeed(result),
619
+ err => c.fail(err)
620
+ )
621
+ }
622
+ val result: String = async.block // waits for the callback; this stub already fired
623
+ ```
488
624
 
489
- `.block` drives an `Async` to its value. A ready value returns immediately. A
490
- pending value blocks the calling thread on the JVM (Loom-friendly) and throws
491
- on JS, where the platform cannot block. Use `.block` only at the edge of your
492
- program, never on a scheduler/reactor thread.
625
+ </TabItem>
626
+ <TabItem value="scala3" label="Scala 3">
493
627
 
494
628
  ```scala
495
- val result: Int = Async.succeed(20).map(_ + 1).block
496
- // result: Int = 21
629
+ import zio.blocks.async._
630
+
631
+ def legacyApi(onSuccess: String => Unit, onError: Throwable => Unit): Unit =
632
+ onSuccess("done")
633
+
634
+ val async: Async[String] = Async.promise[String] { c ?=>
635
+ legacyApi(
636
+ result => c.succeed(result),
637
+ err => c.fail(err)
638
+ )
639
+ }
640
+ val result: String = async.block // waits for the callback; this stub already fired
497
641
  ```
498
642
 
499
- ### Eager, cancellable running: `Async.start` and `Async.Running`
643
+ </TabItem>
644
+ </Tabs>
500
645
 
501
- The `fa.start` extension eagerly drives an already-built `Async` without
502
- blocking and returns a `Running[A]` handle — itself an `Async[A]` you can poll,
503
- compose, or cancel. Compose with `either`, `tap`, `foldCause`, and the other
504
- operators **before** `start` to observe or transform the outcome:
646
+ The two lines inside `legacyApi` are the whole bridge: whichever callback fires, it completes `c`, and completing `c` completes the `Async[String]`. Note what has been gained — `async` is an ordinary value. You can return it, store it, or chain `map` and `flatMap` onto it, and the callback API is no longer visible to anything downstream.
647
+
648
+ The bridge is also safe against a callback that fires more than once — see [`Completer`](#completer).
649
+
650
+ ### Direct Style
651
+
652
+ Inside `Async.async { ... }`, use `await` to extract values from `Async` computations in sequential-looking code without explicit `flatMap` chains:
505
653
 
506
654
  ```scala
507
655
  import zio.blocks.async._
508
656
 
509
- val running: Async.Running[Either[Throwable, Int]] =
510
- Async.succeed(1).map(_ + 1).either.tap {
511
- case Right(value) => Async.succeed(println(s"done: $value"))
512
- case Left(cause) => Async.succeed(println(s"failed: $cause"))
513
- }.start
657
+ case class Order(id: Int, userId: Int)
658
+ case class User(id: Int, name: String, tier: String)
659
+ case class Shipment(orderId: Int, carrier: String)
660
+
661
+ def fetchOrder(id: Int): Async[Order] = Async.succeed(Order(id, 1))
662
+ def fetchUser(id: Int): Async[User] = Async.succeed(User(id, "sam", "gold"))
663
+ def fulfill(orderId: Int): Async[Shipment] = Async.succeed(Shipment(orderId, "express"))
514
664
 
515
- running.cancel() // idempotent; no-op once the run has completed
665
+ def fulfillOrGuest(orderId: Int): Async[String] = Async.async {
666
+ val order = fetchOrder(orderId).catchAll(_ => fetchOrder(9001)).await
667
+ val user = fetchUser(order.userId)
668
+ .catchAll(_ => Async.succeed(User(0, "guest", "bronze"))).await
669
+ val shipment = fulfill(order.id).await
670
+ s"shipped ${shipment.orderId} for ${user.name} via ${shipment.carrier}"
671
+ }
672
+ val result: String = fulfillOrGuest(9001).block
516
673
  ```
517
674
 
518
- The companion `Async.start(body)` is a single by-name method that evaluates
519
- `body` on a background worker (JVM) or microtask (JS) and returns a `Running`
520
- for the result — the `Async` analogue of `Future.apply`. It captures a throwing
521
- body (even a statically `Nothing`-typed one such as `Async.start(sys.error(...))`)
522
- as a failed run rather than letting it escape at the call site. (Driving an
523
- existing `Async` value is the `fa.start` extension above, not `Async.start(fa)`,
524
- which would treat `fa` as a by-name body to evaluate.)
675
+ Awaits run in source order; a failed `Async[A]` under `await` propagates as `Async.fail`.
676
+
677
+ Nothing new happens at runtime here. `Async.async` rewrites its body at compile time: the block is split at each `await` and reassembled into the `flatMap` chain you would have written by hand. The example above compiles to roughly this:
678
+
679
+ ```scala
680
+ fetchOrder(orderId).catchAll(_ => fetchOrder(9001)).flatMap { order =>
681
+ fetchUser(order.userId)
682
+ .catchAll(_ => Async.succeed(User(0, "guest", "bronze")))
683
+ .flatMap { user =>
684
+ fulfill(order.id).map { shipment =>
685
+ s"shipped ${shipment.orderId} for ${user.name} via ${shipment.carrier}"
686
+ }
687
+ }
688
+ }
689
+ ```
525
690
 
526
- For an already-ready `Async`, observers composed before `start` run synchronously
527
- on the calling thread. For a suspended `Async`, driving proceeds on a daemon
528
- worker thread on the JVM, or via microtasks on Scala.js. `cancel()` is
529
- driver-level only: it stops the poll loop and suppresses publishing a terminal
530
- value, but does not abort an in-flight leaf (socket, timer, JS promise).
691
+ So `await` is not a method that blocks or waits. It is a marker the rewrite removes, and everything after it becomes the continuation that runs once the value arrives. Direct style therefore costs nothing over writing the chain yourself — by the time the code runs, it *is* that chain. Choose whichever reads better.
531
692
 
532
- #### Fanning one `Async` out to several consumers
693
+ One consequence is worth remembering: `await` only means something inside an `Async.async` block. Elsewhere there is no rewrite to remove it, and both Scala versions reject it at compile time — Scala 2 through `@compileTimeOnly`, Scala 3 by aborting the macro expansion. You will not ship this mistake.
533
694
 
534
- To deliver one `Async`'s result to multiple consumers, **start it once and share
535
- the `Running` handle** — `Running` publishes its outcome through an atomic, so
536
- the underlying `Async` (and any side effects in `map`/`flatMap`/`tap`) is driven
537
- exactly once no matter how many consumers poll, block, or compose on the handle:
695
+ Within the block, `await` is not restricted to statement position. It also works inside the closures you pass to the strict collections — `List`, `Option`, `Vector`, `Set`, `Map`, `Array`, `Queue`, `ArraySeq` — for `map`, `foreach`, `flatMap`, `filter`, `filterNot`, `collect`, `find`, `exists`, `forall`, `foldLeft`, `foldRight`, `reduce` and `reduceLeft`, and in for-comprehensions over them:
538
696
 
539
697
  ```scala
540
698
  import zio.blocks.async._
541
699
 
542
- val shared: Async.Running[Int] = Async.succeed(1).map(_ + 1).start
543
- val a: Int = shared.block // both observe the one result;
544
- val b: Int = shared.block // the `+ 1` ran once, on the worker
700
+ def fetchName(id: Int): Async[String] = Async.succeed(s"user-$id")
701
+
702
+ val names: Async[List[String]] = Async.async {
703
+ List(1, 2, 3).map(id => fetchName(id).await)
704
+ }
545
705
  ```
546
706
 
547
- Do **not** instead drive the same raw `Async` from two places at once (two
548
- separate `fa.start`s on the same `fa`, or `fa.start` racing `fa.block`). On the
549
- JVM that polls the same combinator concurrently, which is **undefined**: a
550
- `map`/`flatMap`/`tap` function may run more than once, and a `collectAll` batch
551
- may observe its drain buffer mid-update. This matches `Pollable`'s contract that
552
- re-polling a settled value is undefined and platform-specific — it cannot arise
553
- on single-threaded Scala.js. Sequential re-use (re-polling or composing after an
554
- earlier drive has settled) is fine; only *concurrent* re-driving of the raw
555
- value is not.
707
+ Read that list literally; the near neighbours are not all included. `reduceRight` and `reduceOption` are not, and neither is the two-argument `fold`. `takeWhile` and `dropWhile` are, but only over an ordered receiver — `List`, `Vector`, `Queue`, `ArraySeq` or `Array` — because the prefix they compute is meaningless on a `Set` or a `Map`. And two cases surprise people: `Map.filter` with an `await` inside works on Scala 2 only, and a `Map.collect` whose closure yields a pair is unsupported everywhere. Lazy collections are outside the set entirely — force them to a strict collection first.
556
708
 
557
- ## Interop
709
+ The semantics are uniform across all of them. Each is lazy and sequential: the closure for element *n+1* runs only after element *n*'s `await` has completed, so a `List(a, b, c).map(fetch(_).await)` performs three fetches one after another rather than at once — use `Async.collectAll` when you want them overlapped. A failed `await` short-circuits the remainder, and the result keeps the receiver's collection type.
558
710
 
559
- `Async` converts to and from the platform's standard async types, preserving
560
- both success and failure. Ingress lives on the `Async` companion
561
- (`Async.fromFuture`, `Async.fromCompletionStage`, `Async.fromJsPromise`) and
562
- egress lives on extension methods (`fa.toFuture`, `fa.toCompletableFuture`,
563
- `fa.toJsPromise`). `scala.concurrent.Future` conversion is available on both
564
- platforms; the JVM additionally offers Java `CompletionStage` /
565
- `CompletableFuture`, and Scala.js offers `js.Promise`.
711
+ Where a collection method would throw on its own, it still does: `reduce` over an empty receiver fails with `UnsupportedOperationException`, which arrives as an ordinary `Async` failure you can `catchAll`.
566
712
 
567
- On the JVM:
713
+ The rewrite is performed by `dotty-cps-async` on Scala 3 and by a built-in `scala-reflect` macro on Scala 2.13. Both Scala versions support direct style, and neither asks you to add anything to your build.
714
+
715
+ Scala.js 3.8 and later takes a hybrid route, decided per call site. An `await` in direct position compiles to JavaScript's own `async`/`await`, which is the fastest path available; an `await` sitting under a lambda, a by-name argument, or a nested method falls back to the `dotty-cps-async` transform, because the native primitive is not legal in those positions. Nothing about this is yours to configure — the wider `Async.async` surface works either way.
716
+
717
+ ### Bracket and Ensuring
718
+
719
+ Some work has to happen no matter what: closing a file, releasing a connection, deleting a temporary directory. In ordinary code you write that in a `finally` block. `ensuring` is the same idea for `Async`: you attach a cleanup value, and its *outcome* is applied once the computation settles, whether that produced a value or a failure. Note "value", not "thunk" — `ensuring` takes an `Async`, and under the [evaluation model](#evaluation-model) building one runs its synchronous part immediately:
568
720
 
569
721
  ```scala
570
722
  import zio.blocks.async._
571
- import scala.concurrent.{ExecutionContext, Future}
572
- import java.util.concurrent.CompletableFuture
573
-
574
- implicit val ec: ExecutionContext = ExecutionContext.global
575
-
576
- val fromFut: Async[Int] = Async.fromFuture(Future.successful(1))
577
- val toFut: Future[Int] = Async.succeed(1).toFuture
578
- val fromStage: Async[Int] = Async.fromCompletionStage(CompletableFuture.completedFuture(1))
579
- val toStage: CompletableFuture[Int] = Async.succeed(1).toCompletableFuture
580
- ```
581
-
582
- On Scala.js, the companion provides `Async.fromFuture` / `Async.fromJsPromise`
583
- and the egress extensions provide `fa.toFuture` / `fa.toJsPromise` for native
584
- `scala.scalajs.js.Promise` interop.
585
-
586
- ## Low-level building blocks: `Pollable`
587
-
588
- Most code should use the constructors and `Async.promise`. For custom
589
- asynchronous leaves you can implement a `Pollable[A]` directly. `poll(onComplete)`
590
- returns the ready value (or a `Failure`) when available, or a `Pollable`
591
- (commonly `this`, optionally a replacement representing the rest of the
592
- computation — drivers and combinators direct their next poll at whichever
593
- pollable was returned) when still pending; a pending pollable must arrange to
594
- call `onComplete.run()` once progress can be made, prompting the scheduler to
595
- re-poll.
596
-
597
- ### Combinator chain depth
598
-
599
- Combinator continuations poll their children recursively without a trampoline
600
- (a deliberate trade: the poll path stays allocation- and indirection-free). The
601
- determinant of depth-safety is whether driving has to unwind a deep chain of
602
- combinators in **receiver position** over a value that is still **pending** —
603
- not whether the chain was written iteratively or recursively:
604
-
605
- - **Over a ready source, `flatMap` / `map` / `zipWith` chains are depth-safe to
606
- any length.** When the receiver is already a value, each step resolves
607
- eagerly and collapses — no `Pollable` is retained — so `var fa = …;
608
- while (…) fa = fa.flatMap(g)` (and the `.map` form) consume constant stack
609
- regardless of length (verified into the millions).
610
- - **Over a pending source, a deep receiver-position chain is NOT depth-safe.**
611
- When the receiver stays pending, each `fa.flatMap(g)` / `fa.map(g)` /
612
- `fa.zipWith(...)` wraps the previous pending value, so driving descends one
613
- stack frame per level before anything settles and overflows around
614
- default-JVM-stack depths of a few tens of thousands (~50–100k). This is true
615
- for **both** a recursive shape (`def loop(n) = src.flatMap(_ => loop(n-1))`)
616
- **and** an iterative accumulation (`fa = fa.flatMap(_ => src.flatMap(...))`) —
617
- the syntax doesn't matter, the pending receiver spine does.
618
- - **`Async.collectAll` and direct-style `Async.async` `while` loops are
619
- depth-safe even over pending sources.** `collectAll` is a single `Pollable`
620
- that iterates its elements internally (no receiver spine), and the
621
- `Async.async` loop rewrite advances one iteration per driver poll rather than
622
- pre-building a deep chain — both verified at 200k+ pending steps.
623
-
624
- So: to sequence a large, *pending-heavy* workload, reach for `collectAll` or an
625
- `Async.async` `while` loop, not a hand-built `flatMap`/`map`/`zipWith` tower over
626
- a pending value. (`Future` avoids this overflow for any shape only because it
627
- bounces every `flatMap` through its `ExecutionContext`; `Async` skips that hop
628
- for speed and accepts the depth bound instead.)
629
-
630
- ## Cross-platform and cross-version notes
631
-
632
- | Feature | JVM | JS | Scala 2.13 | Scala 3.x | Notes |
633
- |----------------------------------|-----|----|------------|-----------|---------------------------------------------------------|
634
- | Constructors & transformers | ✅ | ✅ | ✅ | ✅ | Identical behavior everywhere |
635
- | `Async.async` / `.await` | ✅ | ✅ | ✅ | ✅ | DCA (Scala 3), native `js.async`/`js.await` for direct-position awaits with DCA fallback for closure/by-name awaits (3.8+ JS), macro (Scala 2); `.await` in the standard strict-collection HOF closures (`List` / `Option` / `Vector` / `Set` / `Map` / `Array` / `Queue` / `ArraySeq`: `map`/`foreach`/`flatMap`/`filter`/`collect`/`fold*`/`reduce*`/`takeWhile`/`dropWhile`/`find`/`exists`/`forall`) and their for-comprehensions is supported on every cell, except a few explicitly-noted divergences (`Map.filter` Scala-2-only; a pair-yielding `Map.collect` unsupported everywhere) — see the HOF section above |
636
- | `.block` on a pending value | ✅ | ❌ | ✅ | ✅ | Blocks on JVM; throws on JS (cannot block) |
637
- | `Async.start` / `Async.Running` | ✅ | ✅ | ✅ | ✅ | Eager non-blocking runner; worker thread (JVM) / microtask (JS) |
638
- | `Future` interop | ✅ | ✅ | ✅ | ✅ | `Async.fromFuture` / `fa.toFuture` on both platforms |
639
- | `CompletionStage` interop | ✅ | ❌ | ✅ | ✅ | JVM-only (`fromCompletionStage` / `toCompletableFuture`) |
640
- | `js.Promise` interop | ❌ | ✅ | ✅ | ✅ | JS-only (`fromJsPromise` / `toJsPromise`) |
641
-
642
- The core `Async` API is identical across all platforms and Scala versions by
643
- design; platform interop APIs are intentionally platform-specific as shown
644
- above. The cross-platform test suite fails if any user-visible core behavior
645
- diverges.
723
+
724
+ val result: Async[String] =
725
+ Async.attempt(openResource()).flatMap { res =>
726
+ Async.attempt(res.read()).ensuring(Async.attempt(res.close()))
727
+ }
728
+ ```
729
+
730
+ That example is safe only because `Async.attempt(res.read())` is already finished by the time `ensuring` is reached. Written over something that genuinely waits, the same shape closes the resource while the read is still in flight:
731
+
732
+ ```scala
733
+ // WRONG: res.close() runs here, as the argument is built —
734
+ // not after the read completes.
735
+ readAsync(res).ensuring(Async.attempt(res.close()))
736
+ ```
737
+
738
+ To defer the effect itself, put it somewhere that is only run when driven — a `flatMap` or `tap` closure, or a finalizer that is genuinely suspended:
739
+
740
+ ```scala
741
+ readAsync(res).flatMap(v => Async.attempt(res.close()).as(v))
742
+ ```
743
+
744
+ What `ensuring` does guarantee is the rule worth remembering: **the cleanup never changes the answer.** It cannot turn a failure into a success, and it cannot turn a success into a failure.
745
+
746
+ That last part raises an obvious question — what if the cleanup itself fails? Closing a file can throw too. The answer depends on how the main computation ended, so it is worth seeing both cases:
747
+
748
+ ```scala
749
+ import zio.blocks.async._
750
+
751
+ // Both fail: reading the resource, and then closing it.
752
+ val bothFail: Async[String] =
753
+ Async
754
+ .attempt[String](throw new RuntimeException("read failed"))
755
+ .ensuring(Async.attempt(throw new IllegalStateException("close failed")))
756
+
757
+ bothFail.either.block match {
758
+ case Left(e) =>
759
+ println(e.getMessage) // read failed <- the original failure
760
+ println(e.getSuppressed()(0).getMessage) // close failed <- attached to it
761
+ case Right(_) => ()
762
+ }
763
+
764
+ // Only the cleanup fails.
765
+ val readOk: Async[String] =
766
+ Async
767
+ .succeed("contents")
768
+ .ensuring(Async.attempt(throw new IllegalStateException("close failed")))
769
+
770
+ val value: String = readOk.block // "contents" — the close failure is gone
771
+ ```
772
+
773
+ In the first case the read had already failed, so you get the read's exception — the one that explains what actually went wrong. The close error is not thrown away, though: it is carried along inside that exception, in a list the JVM keeps for exactly this purpose. `getSuppressed` returns that list. Your logging framework almost certainly prints it, usually under a line beginning `Suppressed:`, so both problems end up on the page.
774
+
775
+ The second case is the one to watch. The read succeeded, so there is no exception to carry the close error, and it is simply dropped — `readOk.block` returns `"contents"` and you never hear that closing failed. If a cleanup error matters to you on the success path, catch it inside the cleanup step itself and log it there:
776
+
777
+ ```scala
778
+ Async
779
+ .attempt(res.read())
780
+ .ensuring(Async.attempt(res.close()).catchAll { t =>
781
+ Async.succeed(logger.warn("close failed", t))
782
+ })
783
+ ```
784
+
785
+ ### Concurrent Fan-Out via Running
786
+
787
+ Suppose one expensive computation feeds several parts of your program — a report that is both summarised and emailed, say. The obvious approach is to build the work once and use that value in both places — but driving it is what produces the result, so each place that drives it does the work again. You want the work to happen once, in the background, with everyone reading the same outcome.
788
+
789
+ `Async.start` does that. It hands the body to a background worker and returns immediately with an `Async.Running[A]` — a handle to work already in flight:
790
+
791
+ ```scala
792
+ import zio.blocks.async._
793
+
794
+ def heavyComputation(): Int = { Thread.sleep(50); 42 }
795
+
796
+ // Returns straight away; the work proceeds on a background worker.
797
+ val running: Async.Running[Int] = Async.start(heavyComputation())
798
+
799
+ // The handle is itself an Async[Int], so it composes like anything else.
800
+ val doubled: Async[Int] = running.map(_ * 2)
801
+ val labelled: Async[String] = running.map(n => s"got $n")
802
+
803
+ val a: Int = doubled.block // 84
804
+ val b: String = labelled.block // "got 42" — heavyComputation ran once, not twice
805
+ ```
806
+
807
+ Both consumers see the same settled outcome, because they share one running computation rather than one recipe. `Async.Running[A]` is a subtype of `Async[A]`, so it works with `map`, `flatMap`, and `zipWith` without conversion, and `running.cancel()` stops the driver, which is [less than it sounds](#cancelable).
808
+
809
+ Sharing the handle is not merely tidier than the alternative — the alternative is unsafe. Driving the same raw `Async` from two places at once is **undefined behaviour**: two `fa.start` calls on the same `fa`, or an `fa.start` racing an `fa.block`. On the JVM that polls the same combinator concurrently, so a function you passed to `map`, `flatMap`, or `tap` may run more than once, and a `collectAll` may read its drain buffer mid-update. Start once, share the `Running`.
810
+
811
+ Sequential re-use is fine — polling or composing a value again after an earlier drive has settled is well defined. Only *concurrent* driving of the same raw value is not, and single-threaded Scala.js cannot hit it at all.
812
+
813
+ Take care to start the work the right way round, because the wrong version looks almost identical:
814
+
815
+ ```scala
816
+ Async.start(heavyComputation()) // ✅ the worker evaluates it
817
+ Async.attempt(heavyComputation()).start // ❌ already evaluated, on this thread
818
+ ```
819
+
820
+ Both lines compile, and both hand you an `Async.Running[Int]`. Only the first one runs anything in the background.
821
+
822
+ The difference is *when the argument gets evaluated*. Scala normally evaluates an argument before passing it, so in `Async.attempt(heavyComputation())` the computation runs first — on your own thread, right at that line — and `attempt` merely wraps the answer it produced. Tacking `.start` on afterwards cannot un-run it; there is nothing left to move to a worker.
823
+
824
+ `Async.start` is declared differently. Its parameter is `body: => A`, and that `=>` means "don't evaluate this yet — hand me the code and I will run it when I am ready." It passes the code to a worker thread, which is why the call returns immediately.
825
+
826
+ The clock shows it plainly:
827
+
828
+ ```scala
829
+ Async.start(heavyComputation()) // returns in about 0 ms
830
+ Async.attempt(heavyComputation()).start // returns in about 50 ms — you waited for it
831
+ ```
832
+
833
+ So: use `Async.start` for work you want moved off the calling thread. Use `fa.start` when `fa` is an `Async` you have already built and composed and now want driven.
834
+
835
+ ### Batch Collection
836
+
837
+ Use `Async.collectAll` to sequence a list of `Async` values and gather results into a `List[A]` in input order:
838
+
839
+ ```scala
840
+ import zio.blocks.async._
841
+
842
+ val batch: Async[List[Int]] = Async.collectAll(List(
843
+ Async.attempt(compute(1)),
844
+ Async.attempt(compute(2)),
845
+ Async.attempt(compute(3))
846
+ ))
847
+ val results: List[Int] = batch.block // => List(r1, r2, r3) in input order
848
+ ```
849
+
850
+ The first failure short-circuits and remaining elements are not driven. Already-ready lists take an optimized path that skips allocating a sequencing continuation.
851
+
852
+ ## Integration Points
853
+
854
+ You are unlikely to be starting from scratch. Your codebase probably already returns `Future`s, calls a Java library that returns a `CompletionStage`, or talks to a JavaScript API that returns a `Promise`. `Async` is built to sit next to those, so you can adopt it in one part of a program without rewriting everything around it.
855
+
856
+ Conversions go in both directions, and none of them blocks a thread:
857
+
858
+ | You have | Bring it in with | You need | Hand it out with |
859
+ |----------------------------|---------------------------------|------------------------------|--------------------------|
860
+ | `Future[A]` | `Async.fromFuture(f)` | `Future[A]` | `fa.toFuture` |
861
+ | `CompletionStage[A]` (JVM) | `Async.fromCompletionStage(cs)` | `CompletableFuture[A]` (JVM) | `fa.toCompletableFuture` |
862
+ | `js.Promise[A]` (Scala.js) | `Async.fromJsPromise(p)` | `js.Promise[A]` (Scala.js) | `fa.toJsPromise` |
863
+
864
+ A round trip through `Future` looks like this — take what an existing service hands you, work with it as an `Async`, and give a `Future` back to a caller who still expects one:
865
+
866
+ ```scala
867
+ import zio.blocks.async._
868
+ import scala.concurrent.{ExecutionContext, Future}
869
+ import scala.concurrent.ExecutionContext.Implicits.global
870
+
871
+ // The service you already have.
872
+ def loadUserName(id: Int): Future[String] = Future.successful("sam")
873
+
874
+ // Bring it in, work with it as an Async, hand a Future back out.
875
+ def greet(id: Int): Future[String] =
876
+ Async
877
+ .fromFuture(loadUserName(id))
878
+ .map(name => s"hello, $name")
879
+ .catchAll(_ => Async.succeed("hello, guest"))
880
+ .toFuture
881
+ ```
882
+
883
+ Java's `CompletionStage` works the same way:
884
+
885
+ ```scala
886
+ import zio.blocks.async._
887
+ import java.util.concurrent.{CompletableFuture, CompletionStage}
888
+ import scala.concurrent.ExecutionContext.Implicits.global
889
+
890
+ def fetchToken(): CompletionStage[String] = CompletableFuture.completedFuture("t-123")
891
+
892
+ val token: Async[String] = Async.fromCompletionStage(fetchToken())
893
+ val backToJava: CompletableFuture[String] = token.map(_.toUpperCase).toCompletableFuture
894
+ ```
895
+
896
+ On Scala.js the pair is `fromJsPromise` and `toJsPromise`:
897
+
898
+ ```scala
899
+ import zio.blocks.async._
900
+ import scala.scalajs.js
901
+
902
+ def fetchJson(url: String): js.Promise[String] = js.native
903
+
904
+ val parsed: Async[String] = Async.fromJsPromise(fetchJson("/api/config"))
905
+ val handedBack: js.Promise[String] = parsed.map(_.trim).toJsPromise
906
+ ```
907
+
908
+ Two details are worth knowing before you use them.
909
+
910
+ Handing a value out to `Future` or `CompletableFuture` needs an `ExecutionContext` in scope, exactly as ordinary `Future` code does — usually `import scala.concurrent.ExecutionContext.Implicits.global`, as above, or whichever one your application already provides. `toJsPromise` needs nothing, because JavaScript has a single built-in event loop to run the callback on.
911
+
912
+ Failures survive the trip. That takes some care on the Java side: when a `CompletionStage` fails, Java wraps your exception in a `CompletionException` before handing it over. `fromCompletionStage` unwraps it, so the `Async` fails with the exception you actually threw rather than with Java's wrapper — which means `catchAll` sees what you expect.
913
+
914
+ Two further integration points are worth knowing about:
915
+
916
+ **Cancelling with `Using`.** `Async.Running` is an `AutoCloseable` — `Cancelable` extends it — so `scala.util.Using` (or Java's try-with-resources) cancels the work automatically when the block ends, the same way it closes a file handle:
917
+
918
+ ```scala
919
+ import zio.blocks.async._
920
+ import scala.util.Using
921
+
922
+ def pollForUpdates(): Nothing = { while (true) Thread.sleep(100); ??? }
923
+
924
+ Using(Async.start(pollForUpdates())) { running =>
925
+ // Do other work while the poller runs.
926
+ Thread.sleep(500)
927
+ } // leaving the block cancels the driver, whether or not the body threw —
928
+ // the loop itself keeps running; see Cancelable
929
+ ```
930
+
931
+ The [Scope reference](resource-management/scope.md) covers the wider resource-management model.
932
+
933
+ **Feeding [streams](streams/core/stream.md).** A callback-based source can be turned into a stream with `Async.promise` and a `Completer`. Because a stream pulls values as it is ready for them, a source that produces faster than the consumer can handle will not overwhelm it.
934
+
935
+ ## Custom Suspension
936
+
937
+ A result that is not here yet arrives in one of two ways: either something tells you when it is ready, or you have to keep asking. This module has a type for each. `Completer[A]` covers being told, and it is the one you will almost always want. `Pollable[A]` covers having to ask, and exists for the sources that leave you no choice.
938
+
939
+ ### Pollable
940
+
941
+ Imagine waiting on something that never calls you back — a non-blocking socket that answers "no data yet" when you read it, a hardware timer you have to check, a native handle that reports progress only when asked. There is no callback to hand a `Completer` to. The only way to learn whether the result has arrived is to *ask*, and to keep asking. The module cannot know how to ask your particular source; only your code knows that.
942
+
943
+ `Pollable[A]` is where you supply that knowledge. It is a single method:
944
+
945
+ ```scala
946
+ abstract class Pollable[+A] {
947
+ def poll(onComplete: Runnable): Async[A]
948
+ }
949
+ ```
950
+
951
+ The driver calls `poll` whenever it gets the chance, and what you return tells it what to do next:
952
+
953
+ - **Ready?** Return `Async.succeed(a)`. The driver takes the value and stops asking.
954
+ - **Not yet?** Return `this` — *and make sure `onComplete` will be run*. The driver does not come back on its own.
955
+ - **Failed?** Return `Async.fail(t)`. The driver takes the failure and stops asking, and it travels downstream like any other failure.
956
+
957
+ `onComplete` is not an optimisation — it is the only thing that gets you polled again. After a `poll` returns `this`, the driver parks and waits for that callback; if nothing ever runs it, a `block` waits forever on the JVM and throws `IllegalStateException` on Scala.js. `Async.never` is precisely a `poll` that returns `this` and never arms it. Either run `onComplete` before returning, or hand it to whatever will know when to check again.
958
+
959
+ Most programs never need any of this. For a callback-based API, `Async.promise` with a `Completer` is simpler and already correct; for blocking I/O, `Async.attempt` on a worker via `Async.start` fits better. Reach for `Pollable` only when the result genuinely has to be checked rather than delivered.
960
+
961
+ Here is the smallest thing that behaves like a real suspension — a value that refuses to be ready for its first two visits:
962
+
963
+ ```scala
964
+ import zio.blocks.async._
965
+
966
+ class Delayed[A](v: A, var ticks: Int) extends Pollable[A] {
967
+ def poll(onComplete: Runnable): Async[A] =
968
+ if (ticks <= 0) Async.succeed(v)
969
+ else { ticks -= 1; onComplete.run(); this }
970
+ }
971
+
972
+ val result: String = new Delayed("done", ticks = 2).block // => "done"
973
+ ```
974
+
975
+ Follow it one visit at a time:
976
+
977
+ | Visit | `ticks` | What `poll` returns | What the driver does |
978
+ |-------|--------:|-------------------------|-------------------------------|
979
+ | 1st | 2 | `this` | Not ready — asks again |
980
+ | 2nd | 1 | `this` | Not ready — asks again |
981
+ | 3rd | 0 | `Async.succeed("done")` | Takes the value, stops asking |
982
+
983
+ Returning `this` means "still me, still waiting." Returning `Async.succeed(v)` means "here it is." And `onComplete.run()` is the nudge that tells the driver to come back soon rather than in its own time. The toy above runs it immediately, which just asks for another visit right away; real code instead hands `onComplete` to whatever it is waiting on — a socket selector, a timer callback — and lets that source run it when something actually happens. The driver can then stay asleep in between, rather than burning a thread asking a question whose answer has not changed. Meanwhile `.block` waits through all three visits and hands you `"done"` at the end.
984
+
985
+ A real implementation replaces the counter with the actual question — has the socket got bytes, has the timer expired — but the shape does not change.
986
+
987
+ Here is a case you are likely to meet. A service starts a long job — rendering a report, transcoding a video, restoring an archive — and gives you back a job id. There is no webhook and no callback: the only way to find out whether it has finished is to call `GET /jobs/{id}` and look at the status. That is a `Pollable`:
988
+
989
+ ```scala
990
+ import zio.blocks.async._
991
+
992
+ sealed trait JobStatus
993
+ case object Pending extends JobStatus
994
+ case class Done(url: String) extends JobStatus
995
+ case class Failed(reason: String) extends JobStatus
996
+
997
+ // The API you are given: you can ask it, it will never tell you.
998
+ def checkJob(id: String): JobStatus = Done("https://example.invalid/report.pdf")
999
+
1000
+ def download(url: String): Unit = ()
1001
+
1002
+ // Run `task` once, later, without holding on to a thread in the meantime.
1003
+ def scheduleIn(ms: Long, task: Runnable): Unit = {
1004
+ val t = new Thread(() => { Thread.sleep(ms); task.run() })
1005
+ t.setDaemon(true)
1006
+ t.start()
1007
+ }
1008
+
1009
+ final class JobPollable(id: String) extends Pollable[String] {
1010
+ def poll(onComplete: Runnable): Async[String] =
1011
+ checkJob(id) match {
1012
+ case Done(url) => Async.succeed(url)
1013
+ case Failed(reason) => Async.fail(new RuntimeException(s"job $id failed: $reason"))
1014
+ case Pending =>
1015
+ // Nothing will announce the change, so arrange our own next look.
1016
+ scheduleIn(2000, onComplete)
1017
+ this
1018
+ }
1019
+ }
1020
+
1021
+ // From here on it is an ordinary Async: compose it, start it, cancel it.
1022
+ val reportUrl: Async[String] = new JobPollable("job-42")
1023
+ val saved: Async[Unit] = reportUrl.map(url => download(url))
1024
+ ```
1025
+
1026
+ Three things to notice. The status check happens inside `poll`, so it runs only when the driver visits — you are not running a loop of your own. The `Pending` branch schedules the next visit two seconds out, which is what stops this from hammering the service. And a failed job becomes `Async.fail`, so the error travels the same path as every other failure and `catchAll` can recover it.
1027
+
1028
+ Note also what is *not* in the example: no blocking wait, no lock, no shared mutable state. Polling puts you in charge of when the check happens, which is the reason to choose it. Because a `Pollable[A]` can be used wherever an `Async[A]` is expected, the value drops straight into any composition and works with every combinator.
1029
+
1030
+ ### The Pollable Protocol
1031
+
1032
+ Everything above is what a `Pollable` looks like from the outside. This is the contract a driver and an implementation hold each other to, and it matters only if you are on one side of it — writing a `poll`, or writing a driver of your own. If you are composing `Async` values and running them with `block`, `start`, or an interop converter, the built-in drivers already honour every rule below and you can skip the section.
1033
+
1034
+ **Polling is one-shot.** A driver keeps polling only while `poll` returns a still-pending `Pollable`, and must stop as soon as it returns a terminal result — a raw value, or a failed `Async`. Re-polling a `Pollable` after that is outside the contract: it is not guaranteed to be a pure re-observation, a continuation may run a second time, and diagnostic state may be repeated.
1035
+
1036
+ **Identity carries meaning.** A still-pending result is either this pollable itself — the common case — or a *replacement* pollable standing for the rest of the computation, and the caller directs its next poll at whatever came back. Which of the two it is decides what the caller may do next:
1037
+
1038
+ | What `poll` returns | What it means | What the driver does next |
1039
+ |------------------------------|-------------------------------------|----------------------------------------|
1040
+ | `this` — the same identity | Pending; `onComplete` is registered | May wait for that callback |
1041
+ | A different `Pollable` | Synchronous progress, not readiness | Polls the replacement, without waiting |
1042
+ | A value, or a failed `Async` | Terminal | Stops polling |
1043
+
1044
+ The middle row is the one that trips people up. An identity change is *not* a readiness event, so a caller or a wrapper must never synthesize an `onComplete` call for it — it means "there is more to do right now", and waiting on a callback that nobody will run is how such a wrapper hangs.
1045
+
1046
+ **Wakes are permits, not proofs.** Real callbacks may be stale, reentrant, or duplicated. `onComplete` is therefore a coalescible wake permit — "look again" — rather than proof that any particular generation has completed. A driver re-polls and lets `poll` decide; an implementation is free to run `onComplete` more times than strictly necessary without breaking anything, though never fewer.
1047
+
1048
+ **Cancellation reaches the leaf.** Alongside `poll`, a `Pollable` may override `cancel()`, which the driver signals on the active pending operation when cancellation wins the race against completion. The default is a no-op, so a source with abortable work has to supply it — and it must be idempotent and non-blocking, because cancellation never waits:
1049
+
1050
+ ```scala
1051
+ import zio.blocks.async._
1052
+ import java.util.concurrent.atomic.AtomicBoolean
1053
+
1054
+ // `register` stands in for handing the waker to the real source: a socket
1055
+ // selector, a timer, a third-party library's callback slot.
1056
+ final class AbortableRead(register: Runnable => Unit) extends Pollable[Array[Byte]] {
1057
+ private val aborted = new AtomicBoolean(false)
1058
+
1059
+ def poll(onComplete: Runnable): Async[Array[Byte]] =
1060
+ if (aborted.get()) Async.fail(new java.io.IOException("read aborted"))
1061
+ else { register(onComplete); this }
1062
+
1063
+ // Idempotent and non-blocking: it records the intent and returns. The
1064
+ // driver has already stopped listening by the time this runs.
1065
+ override def cancel(): Unit = aborted.set(true)
1066
+ }
1067
+ ```
1068
+
1069
+ The full contract lives in the scaladoc of `Pollable#poll`; [`Cancelable`](#cancelable) covers what cancelling does and does not stop.
1070
+
1071
+ ### Completer
1072
+
1073
+ Polling is the awkward case. Far more often the source does call you back — that is what `Completer[A]` is for, and why you will reach for it and not `Pollable`.
1074
+
1075
+ `Completer[A]` is a `Pollable[A]` that is already written: instead of implementing "is it ready?", you hold a value someone else completes exactly once. It is thread-safe, and the first call to `Completer#succeed` or `Completer#fail` wins while every later call does nothing — so a callback that fires twice cannot corrupt the result.
1076
+
1077
+ The structural declaration is:
1078
+
1079
+ ```scala
1080
+ final class Completer[A] extends Pollable[A] {
1081
+ def succeed(a: A): Unit
1082
+ def fail(cause: Throwable): Unit
1083
+ def peek: Async[A]
1084
+ def poll(onComplete: Runnable): Async[A]
1085
+ }
1086
+ ```
1087
+
1088
+ `Async.promise` creates a new `Completer[A]`, passes it to the body, and returns the `Completer` as an `Async[A]` that the driver polls until the callback fires. If the body happens to complete it before returning — a cache hit, a callback that fires inline — the result collapses to a plain ready value and no `Pollable` is allocated at all. It is the only place in the module where the *code you write* differs between Scala versions — elsewhere the signatures differ but the call sites are identical:
1089
+
1090
+ ```scala
1091
+ // Scala 2 — the completer is an ordinary function parameter
1092
+ def promise[A](body: Completer[A] => Unit): Async[A]
1093
+
1094
+ // Scala 3 — the completer is a context parameter of the body
1095
+ inline def promise[A](inline body: Completer[A] ?=> Unit): Async[A]
1096
+ ```
1097
+
1098
+ The `?=>` on Scala 3 makes the `Completer` a given inside the body rather than a plain argument, which is why the body is written `{ c ?=> ... }` there and `{ c => ... }` on Scala 2. (The two `inline` keywords are what splice the body into the call site instead of allocating a function object — the same technique behind the allocation-free ready path described under [Evaluation Model](#evaluation-model).)
1099
+
1100
+ Being a given is not just bookkeeping: it buys you top-level `succeed` and `fail` helpers that find the completer themselves, so on Scala 3 the bridge need not name it at all.
1101
+
1102
+ ```scala
1103
+ // Scala 3 only — `succeed` and `fail` take the Completer as a given.
1104
+ val fetched: Async[Int] = Async.promise[Int] { c ?=>
1105
+ legacyLookup(onOk = value => succeed(value), onErr = cause => fail(cause))
1106
+ }
1107
+ ```
1108
+
1109
+ Naming it, as the examples below do, is equally valid and reads better when the callback is registered several lines away from where it fires.
1110
+
1111
+ <Tabs groupId="scala-version" defaultValue="scala2">
1112
+ <TabItem value="scala2" label="Scala 2">
1113
+
1114
+ ```scala
1115
+ import zio.blocks.async._
1116
+
1117
+ val async: Async[Int] = Async.promise[Int] { c =>
1118
+ new Thread(() => { Thread.sleep(20); c.succeed(42) }).start()
1119
+ }
1120
+ val result: Int = async.block // => 42
1121
+ ```
1122
+
1123
+ </TabItem>
1124
+ <TabItem value="scala3" label="Scala 3">
1125
+
1126
+ ```scala
1127
+ import zio.blocks.async._
1128
+
1129
+ val async: Async[Int] = Async.promise[Int] { c ?=>
1130
+ new Thread(() => { Thread.sleep(20); c.succeed(42) }).start()
1131
+ }
1132
+ val result: Int = async.block // => 42
1133
+ ```
1134
+
1135
+ </TabItem>
1136
+ </Tabs>
1137
+
1138
+ Now a case from the JDK rather than a sleeping thread. `AsynchronousFileChannel` reads a file without blocking, and reports the outcome through a `CompletionHandler` with two methods: `completed` when the bytes arrive, `failed` when the read goes wrong. Those two are exactly `succeed` and `fail`, so the bridge is almost mechanical:
1139
+
1140
+ ```scala
1141
+ import zio.blocks.async._
1142
+ import java.nio.ByteBuffer
1143
+ import java.nio.channels.{AsynchronousFileChannel, CompletionHandler}
1144
+ import java.nio.file.{Path, StandardOpenOption}
1145
+
1146
+ def readChunk(path: Path, size: Int): Async[ByteBuffer] = {
1147
+ val completer = new Completer[ByteBuffer]
1148
+ val channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)
1149
+ val buffer = ByteBuffer.allocate(size)
1150
+
1151
+ channel.read(buffer, 0L, buffer, new CompletionHandler[Integer, ByteBuffer] {
1152
+ def completed(bytesRead: Integer, buf: ByteBuffer): Unit = {
1153
+ buf.flip()
1154
+ completer.succeed(buf) // the read finished
1155
+ }
1156
+ def failed(cause: Throwable, buf: ByteBuffer): Unit =
1157
+ completer.fail(cause) // the read went wrong
1158
+ })
1159
+
1160
+ completer.peek // hand the pending result to the caller
1161
+ }
1162
+
1163
+ // An ordinary Async from here on.
1164
+ val firstBytes: Async[Int] = readChunk(Path.of("data.bin"), 1024).map(_.remaining)
1165
+ ```
1166
+
1167
+ This is the same bridge as `Async.promise`, written out by hand: create the `Completer`, give its two methods to the callback, and return `completer.peek` as the `Async[ByteBuffer]` the caller waits on. `Async.promise` packages exactly those three steps, so the same function written with it is shorter:
1168
+
1169
+ ```scala
1170
+ import zio.blocks.async._
1171
+ import java.nio.ByteBuffer
1172
+ import java.nio.channels.{AsynchronousFileChannel, CompletionHandler}
1173
+ import java.nio.file.{Path, StandardOpenOption}
1174
+
1175
+ def readChunk(path: Path, size: Int): Async[ByteBuffer] =
1176
+ Async.promise[ByteBuffer] { c ?=>
1177
+ val channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)
1178
+ val buffer = ByteBuffer.allocate(size)
1179
+
1180
+ channel.read(buffer, 0L, buffer, new CompletionHandler[Integer, ByteBuffer] {
1181
+ def completed(bytesRead: Integer, buf: ByteBuffer): Unit = {
1182
+ buf.flip()
1183
+ c.succeed(buf)
1184
+ }
1185
+ def failed(cause: Throwable, buf: ByteBuffer): Unit =
1186
+ c.fail(cause)
1187
+ })
1188
+ }
1189
+ ```
1190
+
1191
+ The completer is created for you and named `c`, and there is no `peek` at the end — `promise` returns the `Async` itself.
1192
+
1193
+ Prefer this version. Write the completer out by hand when the registration does not fit neatly in a single block, when you need to keep the completer around to complete it from elsewhere, or when you want identical source on Scala 2 and Scala 3 — `new Completer[A]` has no context-function syntax to differ over.
1194
+
1195
+ `readChunk` returns before a single byte has been read. Nothing blocks, no thread waits, and the caller receives an `Async[ByteBuffer]` that behaves like any other — `map` it, `zipWith` another read, recover it with `catchAll`, or `block` on it at the edge of the program.
1196
+
1197
+ The once-only guarantee earns its keep in code like this. You are trusting a third-party library to call your handler correctly; if a buggy or retrying implementation calls `completed` twice, or calls both `completed` and `failed`, the first call still decides the outcome and the rest are ignored. You do not have to defend against it yourself.
1198
+
1199
+ `Completer#peek` returns the `Completer` itself as an `Async[A]`, bypassing the `Async.promise` body — useful when managing scheduling manually, as shown in the `delayed` helper under [How They Work Together](#how-they-work-together).
1200
+
1201
+ ## Controlling In-Flight Work
1202
+
1203
+ Once `start` has handed you an `Async.Running[A]`, the computation is being driven for you — by the platform driver if it still has waiting to do, and already settled if it does not. These two types are how you keep a grip on it: `Async.Running` is the handle, and `Cancelable` is the ability to stop what it refers to.
1204
+
1205
+ ### Async.Running
1206
+
1207
+ `Async.Running[A]` is the handle returned by `start`. It extends `Pollable[A]`, which makes it an `Async[A]` in its own right, and [`Cancelable`](#cancelable), which is what lets you stop it.
1208
+
1209
+ The structural declaration is:
1210
+
1211
+ ```scala
1212
+ abstract class Running[+A] extends Pollable[A] with Cancelable
1213
+ ```
1214
+
1215
+ Because a `Running` is an `Async[A]`, you can wait for its result with `block`, compose it further with `map` or `flatMap`, or pass it anywhere an `Async[A]` is expected. [Concurrent Fan-Out via Running](#concurrent-fan-out-via-running) walks through that pattern, and why several consumers should share one handle rather than each starting the work themselves.
1216
+
1217
+ What you should not do is call `poll` yourself. It is there for drivers, and its contract — stop at a terminal value, never re-poll a settled one — is easy to violate by hand. Drive a `Running` the same way you drive any other `Async`: `block`, `toFuture`, or composition. There is no `isCompleted`; if you want to know whether it has finished without waiting, keep that flag yourself where you complete the work.
1218
+
1219
+ Calling `cancel` stops the *driver*, and does nothing if the run has already settled. [`Cancelable`](#cancelable) covers how far that reaches; two consequences belong here:
1220
+
1221
+ ```scala
1222
+ import zio.blocks.async._
1223
+
1224
+ val running: Async.Running[Nothing] = Async.never.start
1225
+ running.cancel() // the driver stops polling; no value is ever published
1226
+ ```
1227
+
1228
+ A cancelled run never settles at all — it does not fail, it simply stops. So anything still holding that handle and calling `block` on it waits forever on the JVM, and gets an `IllegalStateException` on Scala.js. Cancel only when you own every consumer of the handle.
1229
+
1230
+ And with `Async.start(body)`, `cancel` stops the driver, not the evaluation of `body`. That block runs to completion regardless — on its own daemon thread on the JVM, on the next microtask on Scala.js — so cancelling that particular `Running` means you have stopped waiting for the result, not that the work behind it has stopped. A suspended `fa.start`, by contrast, does have its active leaf signalled; [`Cancelable`](#cancelable) draws the line.
1231
+
1232
+ A `Running` is also an `AutoCloseable`, so [`scala.util.Using`](#integration-points) cancels it on leaving a block.
1233
+
1234
+ `block` on a pending handle is available on the JVM only — see [Platform Support](#platform-support).
1235
+
1236
+
1237
+ Attach what you want to observe *before* calling `start`, not after. `start` hands the still-waiting part of the value to a driver, and whatever you composed onto it beforehand is part of what that driver runs:
1238
+
1239
+ ```scala
1240
+ import zio.blocks.async._
1241
+
1242
+ // A row that arrives a moment from now, on another thread.
1243
+ def fetchRow(): Async[String] = Async.promise[String] { c ?=>
1244
+ val t = new Thread(() => { Thread.sleep(250); c.succeed("row-1") })
1245
+ t.setDaemon(true)
1246
+ t.start()
1247
+ }
1248
+ def log(msg: String): Unit = ()
1249
+
1250
+ // Before start: the tap is part of what the driver runs, and fires when the
1251
+ // row arrives.
1252
+ val watched: Async.Running[String] =
1253
+ fetchRow().tap(row => Async.attempt(log(s"got $row"))).start
1254
+
1255
+ // After start: this builds a *new* Async that nobody is driving. The tap runs
1256
+ // only if you drive this one too — by blocking on it, or starting it.
1257
+ val bolted: Async[String] =
1258
+ fetchRow().start.tap(row => Async.attempt(log(s"got $row")))
1259
+ ```
1260
+
1261
+ The second version is not a compile error and not a lost value, which is what makes it easy to write by mistake: `bolted` is simply a value nobody has driven, so its `tap` has not run and will not until something asks `bolted` for a result. So if you want to time the fetch, log its progress, or react the moment it fails, the observer has to be inside the value you hand to `start`.
1262
+
1263
+ `either` and `foldCause` matter more, because they decide whether the run counts as failed. Written `fa.either.start`, where `fa` is the value you are about to start, the run always succeeds, carrying a `Left` or a `Right`. Written `fa.start.either`, the run has already failed; you get your `Either`, but everyone else holding that handle still gets the exception.
1264
+
1265
+ All of that assumes there was something to wait for. If the value already holds its answer there is nothing to hand to a driver: `start` wraps it and returns, spawning no worker, and anything you attached ran while you were building the value — see [Evaluation Model](#evaluation-model).
1266
+
1267
+ ### Running#cancel
1268
+
1269
+ A `Running` carries two `cancel` methods. One it inherits from [`Cancelable`](#cancelable) and takes nothing; the other takes a reporter:
1270
+
1271
+ ```scala
1272
+ abstract class Running[+A] extends Pollable[A] with Cancelable {
1273
+ def cancel(): Unit // inherited from Cancelable
1274
+ def cancel(onCleanupFailure: Throwable => Unit): Unit
1275
+ }
1276
+ ```
1277
+
1278
+ They cancel identically. What differs is where a failure thrown by *cleanup* ends up — and cleanup is the one thing cancellation can still fail at. Cancelling signals `cancel()` on the active leaf, and the teardown that follows may be asynchronous and may throw. That failure has nowhere natural to go: the run has been cancelled, so it will never deliver a result, and there is no channel left to carry an exception to whoever was waiting.
1279
+
1280
+ So it is reported instead. `cancel()` sends it to the ambient handler for the platform — the calling thread's `UncaughtExceptionHandler` on the JVM, the queue's failure reporter on Scala.js. `cancel(onCleanupFailure)` sends it to your function, which is what you want whenever "a socket refused to close" should reach a log or a metric rather than stderr:
1281
+
1282
+ ```scala
1283
+ import zio.blocks.async._
1284
+
1285
+ val running: Async.Running[Nothing] = Async.never.start
1286
+
1287
+ running.cancel { cause =>
1288
+ System.err.println(s"cleanup after cancellation failed: ${cause.getMessage}")
1289
+ }
1290
+ ```
1291
+
1292
+ Three properties are worth knowing before you rely on it:
1293
+
1294
+ - **The reporter is kept only if this cancellation wins.** If the run had already settled, or another `cancel` call claimed it first, your function is dropped and never invoked. A dropped reporter is not an error; it means there was no cancellation cleanup of yours to report on.
1295
+ - **It is invoked at most once.** When cleanup fails in more than one place, the later causes are attached to the first as suppressed exceptions and the first is what you are handed — so check `getSuppressed` if you are logging the whole picture.
1296
+ - **It may run on any thread.** Cleanup is driven by whichever party claims it, which is not necessarily the thread that called `cancel`. Keep the reporter short and make sure it cannot throw.
1297
+
1298
+ ### Cancelable
1299
+
1300
+ `Cancelable` is the minimal cancellation interface: one `cancel()` method, safe to call from any thread and safe to call twice.
1301
+
1302
+ Be precise about what it stops. Cancellation does two things, and they reach different distances:
1303
+
1304
+ - **It stops the driver.** The poll loop halts and publication of a terminal value is suppressed. A cancelled run therefore never delivers at all — it does not fail, it simply stays pending forever, which is why anything still calling `block` on that handle waits indefinitely.
1305
+ - **It signals the active pending operation.** When cancellation wins the race against completion, the driver calls [`Pollable.cancel()`](#the-pollable-protocol) on whatever leaf the run is currently suspended on. A leaf that owns abortable work — an in-flight socket read, a timer, a registration with a third-party library — implements that hook and gets the chance to tear it down.
1306
+
1307
+ The second point is the part to get right, because the default hook is a **no-op**. A leaf that does not override `cancel()` is not aborted by cancelling the run: its socket read stays outstanding, its timer still fires, its `js.Promise` still settles. Whether cancellation reaches the work is a property of the leaf, not of the handle you called `cancel()` on.
1308
+
1309
+ Cancellation is also **cooperative**, never preemptive. It does not wait for an already-running `poll` invocation to return, and it interrupts no thread. Calling `cancel()` publishes the signal and returns; whatever teardown that triggers is driven afterwards, on whichever party wins the claim.
1310
+
1311
+ That is why a leaf holding a resource still deserves an explicit finalizer. If a cancelled computation would otherwise leave a socket or a file handle open and you do not control its `Pollable`, close it yourself — pair the cancellation with [`ensuring`](#bracket-and-ensuring), or hold the resource in a `Using` block.
1312
+
1313
+ The structural declaration is:
1314
+
1315
+ ```scala
1316
+ trait Cancelable extends AutoCloseable {
1317
+ def cancel(): Unit
1318
+ final def close(): Unit = cancel()
1319
+ }
1320
+ ```
1321
+
1322
+ `Cancelable.noop` is the predefined no-op instance, useful as a placeholder when no real cancellation is needed:
1323
+
1324
+ ```scala
1325
+ import zio.blocks.async._
1326
+
1327
+ val c: Cancelable = Cancelable.noop
1328
+ c.cancel() // no-op
1329
+ c.close() // no-op; delegates to cancel()
1330
+ ```
1331
+
1332
+ ## AsyncSelector
1333
+
1334
+ `AsyncSelector[A]` is a low-level building block, and that is worth saying before anything else: most code should not reach for it. If what you want is "run N of these at a time and give me results as they land", the streams module already provides it — [`mapPar`](streams/core/stream.md#streammappar), [`mergeAll`](streams/core/stream.md#streammergeall), and `mapParAsync` — with backpressure, ordering rules, and resource cleanup you would otherwise write yourself. The selector exists because those operators needed a primitive underneath them, and the streams concurrency engine is its only production consumer.
1335
+
1336
+ What it gives you is a **repeatable** multi-way wait. You hand it a fixed number of slots, each holding an independently-pending computation. `select` waits until one of them completes, hands you the winning slot index along with its value, and disarms that slot. `replace` arms the same slot with its next computation, and you select again. That loop is the whole point: a fan-in that runs for the life of a connection, a worker pool that keeps N requests in flight, anything where the same slot is re-used thousands of times.
1337
+
1338
+ A hand-rolled race loop can do the first round of that, but not the thousandth. Racing N computations registers a callback on every loser, and the losers survive into the next round, so a fresh callback piles up on each of them every time round. The selector instead keeps **one stable waker per armed slot** for as long as that slot stays armed, no matter how many selections pass over it.
1339
+
1340
+ Selection is round-robin rather than first-past-the-post. The scan starts at the slot *after* the previous winner, so a slot that is continuously eligible — armed, and with a completed computation — wins within at most `armedCount` *successful* selections. No slot can be starved by a faster neighbour.
1341
+
1342
+ The public surface is small:
1343
+
1344
+ ```scala
1345
+ final class AsyncSelector[A] private (slotCount: Int, initial: IndexedSeq[(Int, Async[A])]) extends Cancelable {
1346
+ def size: Int
1347
+ def replace(index: Int, value: => Async[A]): Unit
1348
+ def select: Async[(Int, A)]
1349
+ def shutdown: Async[Unit]
1350
+ override def cancel(): Unit
1351
+ }
1352
+ ```
1353
+
1354
+ You never construct one directly; `Async.selector` does it:
1355
+
1356
+ ```scala
1357
+ def selector[A](inputs: IndexedSeq[Async[A]]): AsyncSelector[A]
1358
+ ```
1359
+
1360
+ The `inputs` sequence fixes the slot count for the life of the selector — `size` reports it, and it never changes, disarmed slots included. Here is the full loop:
1361
+
1362
+ ```scala
1363
+ import zio.blocks.async._
1364
+
1365
+ val selector: AsyncSelector[Int] =
1366
+ Async.selector(Vector(Async.succeed(10), Async.succeed(20), Async.succeed(30)))
1367
+
1368
+ // Whichever slot is eligible, scanning from just after the previous winner.
1369
+ val (firstSlot, firstValue) = selector.select.block
1370
+
1371
+ // Re-arm the slot that just won. Until this runs, that slot sits disarmed
1372
+ // and is skipped by every selection.
1373
+ selector.replace(firstSlot, Async.succeed(firstValue + 1))
1374
+
1375
+ val (secondSlot, secondValue) = selector.select.block
1376
+
1377
+ // Cancel every still-armed loser and join all of their cleanup.
1378
+ selector.shutdown.block
1379
+ ```
1380
+
1381
+ Four rules keep that loop honest:
1382
+
1383
+ - **A slot can only be armed when it is disarmed.** `replace` on a slot that is still armed throws `IllegalStateException("selector slot N is already armed")`, and `replace` after shutdown throws `IllegalStateException("selector is closed")`. The safe pattern is the one above: re-arm the index you were just handed by `select`, and nothing else.
1384
+ - **`value` is by-name for a reason.** The thunk is evaluated lazily and outside the selector's own lock, so constructing the next computation cannot deadlock against a concurrent `select`. A thunk that returns `null` is reified as a failed `Async` carrying `NullPointerException("selector input returned null Async")` rather than corrupting the slot.
1385
+ - **A winner is removed exactly once.** Disarming happens atomically with the win, so the same completion cannot be handed to two selections, and a failure from an input surfaces as a failed `select` rather than poisoning the selector.
1386
+ - **`shutdown` is where cleanup is joined.** It cancels every armed loser, is idempotent, and — importantly — does not complete until cleanup for every still-armed loser has finished. `cancel()` is the `Cancelable` spelling of the same thing: it starts `shutdown` and returns immediately without waiting. Use `cancel()` when the selector is a resource being closed on the way out of a scope, and `shutdown` when you need to know the cleanup actually finished.
1387
+
1388
+ :::warning[`.block` per selection is for the example only]
1389
+ The snippet above blocks once per round so it reads top to bottom. Real code composes `select` like any other `Async` — `flatMap` it, or `await` it inside `Async.async` — and blocks once at the edge, if at all. Blocking inside a driver deadlocks it; see [Driving](#driving).
1390
+ :::
1391
+
1392
+ ## Failure
1393
+
1394
+ `Failure` is how a failed `Async` is represented. You never construct one — `Async.fail` and `Completer#fail` produce it, `catchAll` and `either` recover from it — but it explains why a failure travels through a chain untouched: it extends `Pollable[Nothing]`, and `map` and `flatMap` return it unchanged instead of running their functions.
1395
+
1396
+ ```scala
1397
+ final class Failure private (val cause: Throwable, private[async] val trusted: Boolean) extends Pollable[Nothing] {
1398
+
1399
+ /** Public construction is deliberately an ordinary, untrusted failure. */
1400
+ def this(cause: Throwable) = this(cause, false)
1401
+
1402
+ def poll(onComplete: Runnable): Async[Nothing] = this
1403
+ }
1404
+ ```
1405
+
1406
+ The one-argument constructor is the only one you can call, and it produces an ordinary, untrusted failure. The `trusted` flag is `private[async]`: it marks a failure that entered through one of the library's own internal boundaries — the typed error channel that `zio-blocks-streams` carries in its `Either` — rather than through user code. It changes nothing in the public API: `cause` is the same `Throwable` either way, and every recovery combinator treats both kinds identically. The distinction is visible only to the module's internal fold, which routes a trusted cause down the typed-error path instead of the defect path.
1407
+
1408
+ [`block`](#driving) re-throws `cause`; `catchAll` hands your recovery function the original `Throwable`, unwrapped; [`either`](#error-handling) turns it into a `Left` instead.
1409
+
1410
+ ## Platform Support
1411
+
1412
+ The core API behaves identically everywhere by design, and the cross-platform test suite fails if any user-visible core behaviour diverges. What varies is the interop surface, which is deliberately platform-specific, and the two operations that depend on having a thread:
1413
+
1414
+ | Feature | JVM | Scala.js | How it differs |
1415
+ |------------------------------|:---:|:--------:|--------------------------------------------------------------------|
1416
+ | Constructors and combinators | yes | yes | Identical on both |
1417
+ | `Async.async` / `await` | yes | yes | Different backend per platform — see [Direct Style](#direct-style) |
1418
+ | `block` on a pending value | yes | no | Throws on Scala.js: no thread to park |
1419
+ | `fa.start` / `Async.Running` | yes | yes | Serialized `ForkJoinPool` tasks on the JVM, microtasks on Scala.js |
1420
+ | `Async.start(body)` | yes | yes | Daemon thread on the JVM, the next microtask on Scala.js |
1421
+ | `Future` interop | yes | yes | Same API on both |
1422
+ | `CompletionStage` interop | yes | no | JVM only |
1423
+ | `js.Promise` interop | no | yes | Scala.js only |
1424
+
1425
+ All of it works on Scala 2.13 and Scala 3.
1426
+
1427
+ ### Where Suspended Work Runs
1428
+
1429
+ On the **JVM**, a suspended run is a chain of serialized tasks on `ForkJoinPool.commonPool()`. There is no dedicated worker thread per run and no pool of the module's own: a task exits the moment its current pollable is still pending, and the waker that `poll` registered submits the next task. A run that spends most of its life waiting therefore occupies no thread at all while it waits.
1430
+
1431
+ The one exception is `Async.start(body)`, which evaluates an arbitrary synchronous block rather than driving an `Async`. That block gets a thread of its own — a daemon named `zio-blocks-async-eval`.
1432
+
1433
+ `block` parks with `LockSupport.park` / `unpark` rather than a monitor, which is a deliberate choice for **virtual threads**: a parked virtual thread (JDK 21+) unmounts its carrier instead of pinning it. The module is Loom-*friendly* in this sense, but it is not Loom-based — it configures no virtual-thread executor, and running on a virtual thread is entirely the caller's decision.
1434
+
1435
+ On **Scala.js** there is no thread to hand work to, so scheduling goes through `JSExecutionContext.queue` as microtasks, with a `setTimeout(0)` macrotask escape for work that must yield back to the host event loop. A cap of 1024 consecutive ready resumptions bounds how long a run of already-ready steps can hold the queue before yielding, so a long synchronous chain cannot starve rendering or I/O.
1436
+
1437
+ That is also why `block` cannot work there. A pending value gets one chance to complete synchronously inside `poll`; if it has not, `block` throws:
1438
+
1439
+ ```
1440
+ java.lang.IllegalStateException: Async.block: suspension did not complete synchronously
1441
+ and JavaScript cannot block. Drive the Pollable from a non-blocking entry point instead.
1442
+ ```
1443
+
1444
+ :::note[There is no blocking-operations API]
1445
+ The async module has no `attemptBlocking`, no blocking thread pool, and no way to mark an operation as blocking. `block` is the only "blocking" thing in it, and it is a terminal — the point where you leave `Async` and go back to synchronous code, not a place to put a blocking call. Run genuinely blocking work on a thread you control, for example with `Async.start(body)`, and bridge the result back through the resulting `Running`.
1446
+ :::
1447
+
1448
+ ## Using Async with Streams
1449
+
1450
+ `zio-blocks-streams` depends on this module, so `Async` is not a neighbouring library to streams — it is part of the streams vocabulary. Every cross-platform stream terminal hands you an `Async`, and everything on this page applies to the value you get back.
1451
+
1452
+ One convention travels with it. Stream terminals return `Async[Either[E, Z]]`, never `Async[Z]`, because a stream has a typed error channel and `Async` does not. The two are deliberately kept apart:
1453
+
1454
+ - The stream's **typed** error `E` stays inside the `Either`. A stream that fails with a typed error still completes its `Async` *successfully*, carrying a `Left`.
1455
+ - `Async`'s own untyped `Throwable` channel is reserved for **defects** — a callback that threw, a finalizer that failed, a cleanup failure. Those fail the outer `Async` and never appear as a `Left`.
1456
+
1457
+ So `catchAll` on a stream terminal recovers bugs, not the errors the stream declares. Those you match on:
1458
+
1459
+ ```scala
1460
+ import zio.blocks.async._
1461
+ import zio.blocks.streams._
1462
+
1463
+ val readings: Stream[String, Int] = Stream(12, 7, 30)
1464
+
1465
+ // A terminal is an ordinary Async, so every combinator on this page applies.
1466
+ val summary: Async[String] =
1467
+ readings.runCollectAsync.map(result =>
1468
+ result match {
1469
+ case Right(values) => s"collected ${values.length} readings"
1470
+ case Left(error) => s"typed error: $error"
1471
+ }
1472
+ )
1473
+
1474
+ // At the edge of a JVM `main`, and nowhere else:
1475
+ val text: String = summary.block
1476
+ ```
1477
+
1478
+ `.block` is the edge-of-the-world move described under [Driving](#driving), and the same two limits hold: never inside a stream callback or a `poll`, and never on a value that may still be pending when you are on Scala.js, where it throws. Cross-platform stream code keeps the `Async` and hands it to the host — `toFuture`, `toJsPromise` — or stays inside `Async.async { … }` and uses `await`.
1479
+
1480
+ Cancellation carries across too: cancelling a running stream is the [`Cancelable`](#cancelable) contract on this page, applied to a reader rather than a single leaf.
1481
+
1482
+ [Asynchronous Stream Execution](streams/execution-and-compatibility/async-execution.md) is the reference for all of it — the terminal family, the async source constructors and operators, manual pull and reader ownership, and what cancelling a stream cleans up.
1483
+
1484
+ ## Running the Examples
1485
+
1486
+ The `async-examples` module ships `AsyncShowcaseExample`, a single runnable pipeline exercising the whole module: `Completer`-backed callback bridges, the `Async.async` direct-style DSL, `catchAll` recovery, and a final `block`. Its `fulfillOrGuest` function is the one shown under [Direct Style](#direct-style); the file also carries the helpers it calls, which is what makes it runnable as it stands.
1487
+
1488
+ To run the full example, clone the repository and execute:
1489
+
1490
+ ```
1491
+ sbt "async-examples/run"
1492
+ ```
646
1493
 
647
1494
  ## See Also
648
1495
 
649
- - [Runnable example](#runnable-example) — `async-examples` single-file showcase
650
- - [Combinators](./combinators.md) — `Async#zip` uses the `Tuples` combiner for
651
- automatic tuple flattening.
1496
+ - [Asynchronous Stream Execution](streams/execution-and-compatibility/async-execution.md) — the `Async` terminal family, async source constructors and operators, reader ownership, and stream cancellation
1497
+ - [Stream Reference](streams/core/stream.md) — pull-based streaming with resource safety; use `Async.promise` and `Completer` to bridge callback-based push sources into the pull-based stream model
1498
+ - [Scope Reference](resource-management/scope.md) — compile-time resource safety; `Async.Running` extends `AutoCloseable` and can be used inside `scala.util.Using` or any Scope-managed context for structured cancellation
1499
+ - [Compile-Time Resource Safety with Scope](../guides/compile-time-resource-safety-with-scope.md) — step-by-step tutorial on resource ownership that applies equally to `Async.Running` handles