# Async/Await (Incubating)

version 6.0.0-RC-1

## 1. Introduction

Groovy provides native `async`/`await` support, enabling developers to write concurrent code in a sequential, readable style. Rather than dealing with callbacks, `CompletableFuture` chains, or manual thread management, you express concurrency with two constructs:

- `async { …​ }` — start a closure on a background thread, returning an `Awaitable`

- `await expr` — block until an asynchronous result is available

On JDK 21+, async tasks automatically leverage virtual threads for optimal scalability. On JDK 17–20, a cached thread pool is used as a fallback.

The examples throughout this guide use an online multiplayer card game as a running theme — dealing hands, racing for the fastest play, streaming cards, and managing tournament rounds.

## 2. Getting Started

### 2.1. Your first async/await

`async { …​ }` starts work immediately on a background thread and returns an `Awaitable`. Use `await` to collect the result:

    def deck = ['2♠', '3♥', 'K♦', 'A♣']
    def card = async { deck.shuffled()[0] }
    println "You drew: ${await card}"

### 2.2. Exception handling

`await` unwraps `CompletionException` and `ExecutionException` automatically. Standard `try`/`catch` works exactly as with synchronous code:

    def drawFromEmpty = async {
        throw new IllegalStateException('deck is empty')
    }
    try {
        await drawFromEmpty
    } catch (IllegalStateException e) {
        // Original exception — no CompletionException wrapper
        assert e.message == 'deck is empty'
    }

### 2.3. CompletableFuture interop

`await` works directly with `CompletableFuture`, `CompletionStage`, and `Future` from Java libraries:

    // await works with CompletableFuture from Java libraries
    def future = CompletableFuture.supplyAsync { 'A♠' }
    assert await(future) == 'A♠'

## 3. Running Tasks in Parallel

The real power of async/await appears when you need to run several tasks concurrently and coordinate their results.

### 3.1. Waiting for all: `Awaitable.all()`

Deal cards to multiple players at the same time and wait for all hands:

    // Deal cards to three players concurrently
    def deck = [*1..9,'J','Q','K','A'].collectMany { rank ->
        ['♠','♥','♦','♣'].collect { suit -> "$rank$suit" }
    }.shuffled()
    def index = new AtomicInteger()
    def draw5 = { -> def i = index.getAndAdd(5); deck[i..i+4] }

    def alice = async { draw5() }
    def bob   = async { draw5() }
    def carol = async { draw5() }

    def (a, b, c) = await Awaitable.all(alice, bob, carol)
    assert a.size() == 5 && b.size() == 5 && c.size() == 5

Multi-argument `await` is syntactic sugar for `Awaitable.all()`:

    def a = async { 1 }
    def b = async { 2 }
    def c = async { 3 }

    // Parenthesized multi-arg await (sugar for Awaitable.all):
    def results = await(a, b, c)
    assert results == [1, 2, 3]

### 3.2. Racing: `Awaitable.any()`

Like JavaScript’s `Promise.race()` — settles as soon as the first task settles, returning its result or propagating its error. A task that fails first therefore fails the whole `any`, even if a later task would have succeeded (use `first` if you want the first success instead). Useful for fallback patterns or latency-sensitive code:

    // Race two servers — use whichever responds first
    def primary  = async { Thread.sleep(200); 'primary-response' }
    def fallback = async { 'fallback-response' }

    def response = await Awaitable.any(primary, fallback)
    assert response == 'fallback-response'

### 3.3. First success: `Awaitable.first()`

Like JavaScript’s `Promise.any()` — returns the first successful result, silently ignoring individual failures. Only fails when every task fails:

    // Try multiple sources — use the first that succeeds
    def failing    = async { throw new RuntimeException('server down') }
    def succeeding = async { 'card-data-from-cache' }

    def result = await Awaitable.first(failing, succeeding)
    assert result == 'card-data-from-cache'

### 3.4. Inspecting all outcomes: `Awaitable.allSettled()`

Waits for all tasks to finish (succeed or fail) without throwing. Returns an `AwaitResult` list where each entry has `success`, `value`, and `error` fields:

    def save1 = async { 42 }
    def save2 = async { throw new RuntimeException('db error') }

    def results = await Awaitable.allSettled(save1, save2)
    assert results[0].success && results[0].value == 42
    assert !results[1].success && results[1].error.message == 'db error'

### 3.5. Combinator summary

Combinator | Completes when | On failure | Use case
--- | --- | --- | ---
`Awaitable.all` | All succeed | Fails immediately on first failure (fail-fast) | Gather results from independent tasks
`Awaitable.allSettled` | All complete (success or fail) | Never throws; failures captured in `AwaitResult` list | Inspect every outcome, e.g. partial-success reporting
`Awaitable.any` | First task settles (success or failure) | Propagates the first settlement’s result or error (JS `Promise.race`) | Latency-sensitive races, fastest-response wins
`Awaitable.first` | First task succeeds, or all fail | Throws only when every source fails (aggregate error) | Hedged requests, graceful degradation with fallbacks

| `all` and `allSettled` accept an empty source list and complete with an empty result. `any` and `first` require at least one source (and reject `null` elements), throwing `IllegalArgumentException` otherwise — a "race" or "first success" with no participants has no meaningful result.

## 4. Generators and Streaming

### 4.1. Producing values with `yield return`

An `async` closure containing `yield return` becomes a generator — it lazily produces a sequence of values. The generator runs on a background thread and blocks on each `yield return` until the consumer is ready, providing natural back-pressure:

    def dealCards = async {
        def suits = ['♠', '♥', '♦', '♣']
        def ranks = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
        for (suit in suits) {
            for (rank in ranks) {
                yield return "$rank$suit"
            }
        }
    }
    def cards = dealCards.collect()
    assert cards.size() == 52
    assert cards.first() == 'A♠'
    assert cards.last() == 'K♣'

Generators return a standard `Iterable`, so regular `for` loops and Groovy collection methods (`collect`, `findAll`, `take`, etc.) work out of the box:

    def scores = async {
        for (s in [100, 250, 75]) { yield return s }
    }
    // Generators return Iterable — regular for and collect work
    assert scores.collect { it * 2 } == [200, 500, 150]

## 5. Channels

Channels provide Go-style inter-task communication. A producer sends values into a channel; a consumer receives them as they arrive. The channel handles synchronization and optional buffering — no shared mutable state needed:

    def cardStream = AsyncChannel.create(3)

    // Dealer — sends cards concurrently
    async {
        for (card in ['A♠', 'K♥', 'Q♦', 'J♣']) {
            await cardStream.send(card)
        }
        cardStream.close()
    }

    // Player — receives cards as they arrive
    def hand = []
    for await (card in cardStream) {
        hand << card
    }
    assert hand == ['A♠', 'K♥', 'Q♦', 'J♣']

Channels support unbuffered (rendezvous, `create()`) and buffered (`create(n)`) modes. Sending blocks when the buffer is full; receiving blocks when empty. With virtual threads, this blocking is essentially free.

Since channels implement `Iterable`, they also work with regular `for` loops and Groovy collection methods.

## 6. Channel Composition

`AsyncChannel` supports composable pipeline operations. Each composition method returns a new channel connected to the source by a background task.

### 6.1. filter

    def source = AsyncChannel.create(10)
    def evens = source.filter { it % 2 == 0 }

    async {
        (1..10).each { source.send(it) }
        source.close()
    }

    for await (val in evens) { println val }  // 2, 4, 6, 8, 10

### 6.2. map

    def doubled = source.map { it * 2 }

### 6.3. Chaining

Operations compose naturally:

    def pipeline = source
        .filter { it > 50 }
        .map { it * 2 }

    for await (val in pipeline) { process(val) }

### 6.4. merge

Combines two channels into one, interleaving values as they arrive:

    def merged = channelA.merge(channelB)
    for await (event in merged) { handle(event) }

### 6.5. split

Partitions a channel into two based on a predicate:

    def (highPriority, normal) = orders.split { it.priority > 5 }

### 6.6. tap

Sends a copy of each value to a monitoring channel while passing through unchanged:

    def monitor = AsyncChannel.create(100)
    def output = source.tap(monitor)
    // output gets all values; monitor gets copies for logging

## 7. Channel Select

`ChannelSelect` waits for the first available value from multiple channels — like Go’s `select` statement:

    import groovy.concurrent.ChannelSelect

    def prices = AsyncChannel.create(10)
    def alerts = AsyncChannel.create(10)

    def sel = ChannelSelect.from(prices, alerts)
    def result = await sel.select()
    println "Channel ${result.index}: ${result.value}"

A select takes exactly one value from exactly one channel; the other channels are left untouched. When several channels already have a value waiting, the one listed first is taken. If every channel is closed and drained, `select()` fails with `ChannelClosedException`.

That priority by list order means a channel that always has a value waiting starves the channels listed after it. `fair()` returns a select that rotates instead — each call starts at the channel after the one that last won, like JCSP’s `fairSelect` — so keep the instance across calls:

    def sel = ChannelSelect.from(requests, control).fair()
    while (true) {
        def result = await sel.select()
        // control is served even while requests never runs dry
    }

`random()` chooses uniformly at random among the ready channels instead, as Go’s `select` does. It keeps no state between calls, so it is equally fair from any number of threads, but it gives no bound on how long a ready channel may be passed over; `fair()` guarantees every ready channel is taken within n calls.

### 7.1. Mixed offers

A select may also offer to send — the mixed choice of the CSP literature: "I will send my opener, or take my peer’s if it sends first". Offers are built with the static factories `send` and `receive` and combined with `offers`, mirroring the send cases of Go’s `select`:

    import static groovy.concurrent.ChannelSelect.*

    def result = await offers(send(ping, 1), receive(pong)).select()
    if (result.send) {
        // my opener committed: continue the ping branch
    } else {
        // my peer opened first: result.value is the pong opener
    }

Exactly one offer commits; a committed send behaves exactly like `ping.send(1)`, while a retired one leaves no trace — no buffered residue, no waiting sender — just as a losing receive leaves its channel untouched. `fair()` and `random()` order the scan over offers exactly as they do over channels, and send offers require channels created by `AsyncChannel.create`.

A send offer commits as soon as its channel can accept the value: when a receiver is waiting, or when buffer space holds it. Two peers racing a mixed choice against each other therefore agree on a single opener only over rendezvous (capacity-0) channels, where a send cannot complete unilaterally; over buffered channels with space, each peer’s send offer commits at once under its own select and the peers proceed down different branches of the same conversation.

### 7.2. Preconditions

Each branch may also carry a precondition — the guarded choice of CSP. `when` writes the guard onto the offer itself, as occam, Ada, Erlang and Kotlin do:

    def sel = offers(receive(input).when { held.size() < capacity },   (1)
                     receive(request).when { !held.isEmpty() })        (2)
    while (true) {
        def result = await sel.select()
        if (result.index == 0) {
            held.addLast(result.value)
        } else {
            await reply.send(held.removeFirst())
        }
    }

1 | take input only while there is room
2 | answer a request only while there is something to hand over

A guarded-off offer is not registered on its channel, so nothing is taken from it and nothing is sent to it, but it keeps its position, so `result.index` denotes the same branch either way. Dropping an offer from the argument list instead would renumber every branch after it, silently changing what an index denotes.

The guard is a closure rather than a boolean because it is consulted afresh on every `select` call — that is what lets one select be built once and reused as the state it guards on changes. `when` returns a guarded copy, leaving the original offer alone, and guards conjoin: over a second `when`, both must hold.

`select` also accepts the same preconditions positionally, one flag per offer, which suits a mask computed as a whole:

    def result = await sel.select(held.size() < capacity, !held.isEmpty())

A flag is conjoined with whatever guard its offer carries — both must hold for the offer to be registered — and passing a number of flags other than the number of offers is an `IllegalArgumentException`. Enabling no branch at all is a deadlock rather than a wait, so `select` fails with `IllegalStateException`.

A branch can also be recognised by identity instead of by position: `result.channel` is the channel the winning offer transferred over.

### 7.3. Timeouts

A deadline can be a branch of the select, as it is in Go, Rust, Clojure and Kotlin, instead of an exception thrown around the whole select by `orTimeout`. There are two forms, one for a timeout that re-arms on every round and one for a fixed deadline.

The timer offer `after` arms a fresh timer each time `select` is called and commits, with the `Instant` it fired, if no channel offer could commit first. It is the "wait for work, but not forever" branch of a select that is built once and reused, and because the timer lives inside the call, the held instance keeps its `fair()` rotation from round to round:

    import static groovy.concurrent.ChannelSelect.*

    def sel = offers(receive(work), after(100)).fair()
    while (true) {
        def result = await sel.select()
        if (result.timeout) {
            // nothing arrived within 100 ms of this call: result.value is the Instant
        } else {
            handle(result.value)
        }
    }

A timer offer that loses its round is cancelled with the other losing branches, so it leaves nothing armed. It reports `result.timeout` rather than a channel: `result.channel` is `null` for a timeout, and `result.index` still names the branch. So a select that carries a timer offer and dispatches on `result.channel` must test `result.timeout` first, as the loop above does. Like any offer it can be guarded with `when`, and a guarded-off timer offer arms nothing. Several timer offers give per-branch deadlines.

A timer offer with a positive delay is never ready at the moment the select registers its offers, so the choice policy considers only the channel offers that are ready at once; the timer commits only when none was. A delay that is not positive has already elapsed, so that offer is ready at registration and the policy treats it like any other ready branch: priority takes whichever is listed first, and `fair()` rotates over it. Listed last, `after(0)` is the `default` clause of Go’s `select` — take a transfer if one can complete at once, otherwise proceed without waiting:

    def result = await offers(receive(work), after(0)).select()
    if (result.timeout) {
        // nothing was ready; do something else and come back
    }

The timer channel `AsyncChannel.after` is the fixed-deadline form: a capacity-1 channel that delivers one value, the `Instant` it fired, once the given delay has elapsed from its creation, and then closes, as Go’s `time.After` does. Received from within a select, it is a deadline shared by every round that selects on it:

    def deadline = AsyncChannel.after(5000)
    def sel = ChannelSelect.from(work, deadline)
    while (true) {
        def result = await sel.select()
        if (result.index == 1) break    // five seconds since the deadline was set
        handle(result.value)
    }

Because it is an ordinary channel, a timer channel composes with everything a channel does: it can be guarded with `when`, and once fired `fair()` and `random()` treat it like any other ready channel. A timer channel that loses a select keeps its value: it fires all the same and holds the instant until received. Once that value has been taken, further receives fail with `ChannelClosedException` rather than waiting forever, and `isClosed()` reports that the timer has fired. Closing a timer channel before it fires cancels it.

Both forms accept a `java.time.Duration` in place of milliseconds, and in both a delay that is not positive has already elapsed: the timer channel is created already holding its instant, and the timer offer is ready at registration. That suits a deadline computed from an absolute time that has already passed.

## 8. BroadcastChannel

A `BroadcastChannel` delivers every value to all subscribers (one-to-many), unlike `AsyncChannel` which is point-to-point:

    import groovy.concurrent.BroadcastChannel

    def broadcast = BroadcastChannel.create()
    def sub1 = broadcast.subscribe()
    def sub2 = broadcast.subscribe()

    async {
        broadcast.send('hello')
        broadcast.send('world')
        broadcast.close()
    }

    // Both subscribers receive both messages
    for await (msg in sub1) { println "Sub1: $msg" }
    for await (msg in sub2) { println "Sub2: $msg" }

Late subscribers only receive messages sent after they subscribe.

### 8.1. Flow.Publisher interop

A `BroadcastChannel` can also be exposed as a `java.util.concurrent.Flow.Publisher` via `asPublisher()`. Each call to `subscribe(Flow.Subscriber)` on the returned publisher creates a new per-subscriber `AsyncChannel` under the hood and respects standard Reactive Streams demand (`request(n)`):

    def broadcast = BroadcastChannel.create()
    def publisher = broadcast.asPublisher()

    // Compose with any Reactive Streams operator, or consume with `for await`:
    for await (msg in publisher) {
        println "Got: $msg"
    }

| Backpressure policy. The publisher bridge uses lossless, sender-gated backpressure: `send()` awaits delivery to every live subscriber, and each per-subscriber channel has a bounded buffer (default 16). A subscriber that never calls `request(n)` — or requests slowly — will fill its buffer, after which `broadcast.send(…​)` stalls for all subscribers until that subscriber catches up. In other words, the slowest subscriber controls producer throughput. This matches the point-to-point semantics of `subscribe()` (values are neither dropped nor reordered). If you need decoupled per-subscriber policies (drop-newest, drop-oldest, latest-only, or unbounded buffering), wrap the publisher with a Reactive Streams operator of your choice, or use a subscriber that drains promptly with `request(Long.MAX_VALUE)`.

| Terminal signals. Per Reactive Streams §1.7, terminal completion is not gated by demand: once the underlying `BroadcastChannel` is closed and the per-subscriber buffer is drained, the subscriber receives `onComplete` even if it has no outstanding `request(n)`. Likewise, `onError` for a spec-violating `request(n ⇐ 0)` (§3.9) is signalled through the same single-threaded delivery path as `onNext` / `onComplete`, preserving §1.3 signal serialization; subscribers do not see those signals racing on the caller’s thread.

## 9. Deferred Cleanup with `defer`

The `defer` keyword schedules a cleanup action to run when the enclosing `async` closure completes, regardless of success or failure. Multiple deferred actions execute in LIFO order — last registered, first to run — making it natural to pair resource acquisition with cleanup:

    def log = []
    def task = async {
        log << 'open connection'
        defer { log << 'close connection' }
        log << 'open transaction'
        defer { log << 'close transaction' }
        log << 'save game state'
        'saved'
    }
    assert await(task) == 'saved'
    // Deferred actions run in LIFO order — last registered, first to run
    assert log == ['open connection', 'open transaction', 'save game state',
                   'close transaction', 'close connection']

Deferred actions always run, even when an exception occurs:

    def cleaned = false
    def task = async {
        defer { cleaned = true }
        throw new RuntimeException('save failed')
    }
    try {
        await task
    } catch (RuntimeException e) {
        assert e.message == 'save failed'
    }
    // Deferred actions run even when an exception occurs
    assert cleaned

If a deferred action returns an `Awaitable` or `Future`, the result is awaited before the next deferred action runs, ensuring orderly cleanup of asynchronous resources.

## 10. Structured Concurrency

Structured concurrency ensures that concurrent tasks have clear ownership and bounded lifetimes — no orphaned background work, no silent failures leaking across your application. This idea is gaining momentum across the industry (Java’s JEP 453, Go’s `errgroup`, Kotlin’s coroutine scopes) because it makes concurrent code easier to reason about, test, and debug. Groovy’s `AsyncScope` provides these guarantees today, even on JDK versions before Project Loom’s `StructuredTaskScope` ships as a final API.

`AsyncScope` binds the lifetime of child tasks to a scope. When the scope exits, all children are guaranteed complete (or cancelled). This prevents orphaned tasks and silent failures:

    // Run a tournament round — all tables play concurrently
    def results = AsyncScope.withScope { scope ->
        def table1 = scope.async { [winner: 'Alice',  score: 320] }
        def table2 = scope.async { [winner: 'Bob',    score: 280] }
        def table3 = scope.async { [winner: 'Carol',  score: 410] }
        [await(table1), await(table2), await(table3)]
    }
    // All tables guaranteed complete when withScope returns
    assert results.size() == 3
    assert results.max { it.score }.winner == 'Carol'

By default, the scope uses fail-fast semantics — if any child fails, all siblings are cancelled immediately:

    try {
        AsyncScope.withScope { scope ->
            scope.async { Thread.sleep(5000); 'still playing' }
            scope.async { throw new RuntimeException('player disconnected') }
        }
    } catch (RuntimeException e) {
        // First failure cancels all siblings and propagates
        assert e.message == 'player disconnected'
    }

The scope waits for every child to finish, even without explicit `await` calls:

    def completed = new AtomicInteger(0)
    AsyncScope.withScope { scope ->
        3.times { scope.async { Thread.sleep(50); completed.incrementAndGet() } }
    }
    // All children have completed — even without explicit await
    assert completed.get() == 3

On JDK 25+, scope tracking uses `ScopedValue` for optimal virtual thread performance (no per-thread storage, automatic inheritance). On JDK 17–24, a `ThreadLocal` fallback is used transparently.

## 11. Advanced Topics

### 11.1. Consuming with `for await`

`for await` iterates over any async source. For generators and plain collections, it works identically to a regular `for` loop:

    def topCards = async {
        for (card in ['A♠', 'K♥', 'Q♦']) {
            yield return card
        }
    }
    def hand = []
    for await (card in topCards) {
        hand << card
    }
    assert hand == ['A♠', 'K♥', 'Q♦']

The key value of `for await` is with reactive types (Reactor `Flux`, RxJava `Observable`, `java.util.concurrent.Flow.Publisher`) where it automatically converts the source to a blocking iterable via the adapter SPI. Without `for await`, you would need to call the conversion manually (e.g., `flux.toIterable()`). For generators and plain collections, a regular `for` loop works identically.

The JDK’s `Flow.Publisher` is supported out of the box — no adapter module needed. This means streams exposed by built-in sources such as `Agent.changes()` (see Agent) and `BroadcastChannel.asPublisher()` (see BroadcastChannel), or any third-party publisher using the JDK interfaces, compose directly with `for await` and `await`:

    import groovy.concurrent.Agent

    def agent = Agent.create(0)
    try {
        async {
            3.times { agent.send { it + 1 } }
            agent.shutdown()
        }
        for await (v in agent.changes()) {
            println "value is now $v"
        }
    } finally {
        agent.shutdown()
    }

### 11.2. Timeouts and Delays

Apply a deadline to any task. If it doesn’t complete in time, a `TimeoutException` is thrown:

    def slowPlayer = async { Thread.sleep(5000); 'finally played' }
    try {
        await Awaitable.orTimeoutMillis(slowPlayer, 100)
    } catch (TimeoutException e) {
        // Player took too long — turn forfeited
        assert true
    }

Or use a fallback value instead of throwing:

    def slowPlayer = async { Thread.sleep(5000); 'deliberate move' }
    def move = await Awaitable.completeOnTimeoutMillis(slowPlayer, 'auto-pass', 100)
    assert move == 'auto-pass'

`Awaitable.delay()` pauses without blocking a thread:

    long start = System.currentTimeMillis()
    await Awaitable.delay(100)   // pause without blocking a thread
    assert System.currentTimeMillis() - start >= 90

### 11.3. Framework Adapters

`await` natively understands `Awaitable`, `CompletableFuture`, `CompletionStage`, and `Future`. `for await` natively understands `java.util.concurrent.Flow.Publisher` in addition to regular `Iterable` sources. Third-party async types can be supported by implementing the `AwaitableAdapter` SPI and registering it via `META-INF/services/groovy.concurrent.AwaitableAdapter`.

Drop-in adapter modules are provided for:

- `groovy-reactor` — `await` on `Mono`, `for await` over `Flux`

- `groovy-rxjava` — `await` on `Single`/`Maybe`/`Completable`, `for await` over `Observable`/`Flowable`

For example, without the adapter you must manually convert RxJava types:

    // Without groovy-rxjava — manual conversion
    def result = Single.just('hello').toCompletionStage().toCompletableFuture().join()

With `groovy-rxjava` on the classpath, the conversion is transparent:

    // With groovy-rxjava — adapter handles the plumbing
    def result = await Awaitable.from(Single.just('hello'))

### 11.4. Executor Configuration

By default, `async` uses:

- JDK 21+: a virtual-thread-per-task executor

- JDK 17–20: a cached daemon thread pool (max 256 threads, configurable via `groovy.async.parallelism` system property)

You can override the executor:

    import org.apache.groovy.runtime.async.AsyncSupport
    import java.util.concurrent.Executors

    AsyncSupport.setExecutor(Executors.newFixedThreadPool(4))
    AsyncSupport.setExecutor(null)  // restore platform default
    // or equivalently:
    AsyncSupport.resetExecutor()

### 11.5. Integration with JDK Classes

`await` works with any JDK API that returns a `CompletableFuture`, `CompletionStage`, or `Future`. This means you can combine process execution, asynchronous file I/O, and HTTP calls in a single `async` block — all running concurrently:

    // Task 1: Run a process and await its completion
    def proc = async {
        def p = echoCmd.execute() // echo Hello from Groovy
        await p.onExit()
        p.text.trim()
    }

    // Task 2: Read a file asynchronously
    def fileContent = async {
        await etcPassword.textAsync
    }

    // Task 3: Fetch a web page using Groovy's HttpBuilder
    def webContent = async {
        def http = HttpBuilder.http { baseUri bankUrl }
        http.get('/withdraw/100').body
    }

    // All three tasks run concurrently — collect results
    var (echo, file, html) = await proc, fileContent, webContent
    assert echo == 'Hello from Groovy'
    assert file =~ /sEcrEt/
    assert html == '<html>SUCCESS</html>'

Task 3 uses Groovy’s `HttpBuilder` (from `groovy-http-builder`), which wraps JDK `HttpClient` with a concise DSL. For lower-level control, you can also use `HttpClient` directly:

    import java.net.http.HttpClient
    import java.net.http.HttpRequest
    import java.net.http.HttpResponse

    def webContent = async {
        def client = HttpClient.newHttpClient()
        def req = HttpRequest.newBuilder()
            .uri(URI.create('https://api.example.com/data'))
            .build()
        def resp = await client.sendAsync(req, HttpResponse.BodyHandlers.ofString())
        resp.body()
    }

Key JDK classes that work with `await` out of the box:

JDK class | Method returning async result
--- | ---
`Process` | `onExit()` → `CompletableFuture<Process>`
`HttpClient` | `sendAsync(…​)` → `CompletableFuture<HttpResponse<T>>`
`Path` (via `groovy-nio`) | `getTextAsync()`, `getBytesAsync()`, `writeAsync(…​)` → `CompletableFuture`
`AsynchronousFileChannel` | `read(…​)`, `write(…​)`, `lock(…​)` → `Future<Integer>` / `Future<FileLock>`
`CompletableFuture` | `supplyAsync(…​)`, `allOf(…​)`, `anyOf(…​)` → `CompletableFuture<T>`

### 11.6. Async Closure Factories

Because `async { …​ }` is just an expression that returns an `Awaitable`, you can wrap it in an ordinary method or closure to create a reusable factory:

    // A method that returns an async task — a simple factory
    def dealCard(List deck) {
        async { deck.shuffled()[0] }
    }

    def numPlayers = 3
    // each player gets a card from their own deck
    def cards = (1..numPlayers).collect { dealCard(['A♠', 'K♥', 'Q♦', 'J♣']) }

    def hands = await Awaitable.all(*cards)
    assert hands.size() == 3
    assert hands.every { it in ['A♠', 'K♥', 'Q♦', 'J♣'] }

This is a natural way to build reusable async building blocks without any special framework support.

## 12. Best Practices

### 12.1. Prefer returning values over mutating shared state

Async closures run on separate threads. Mutating shared variables from multiple closures is a race condition:

    // UNSAFE — shared mutation without synchronization
    var count = 0
    def tasks = (1..100).collect { async { count++ } }
    tasks.each { await it }
    // count may not be 100!

Instead, return values and collect results:

    // SAFE — no shared mutation
    def tasks = (1..100).collect { n -> async { n } }
    def results = await Awaitable.all(*tasks)
    assert results.sum() == 5050

When shared mutable state is unavoidable, use the appropriate concurrency-aware type, e.g. `AtomicInteger` for a shared counter, or thread-safe types from `java.util.concurrent` for players concurrently drawing cards from a shared deck.

### 12.2. Choosing the right tool

Feature | Use when…​
--- | ---
`async`/`await` | You have sequential steps involving I/O or blocking work and want code that reads top-to-bottom.
`Awaitable.all` / `any` / `first` | You need to launch independent tasks and collect all results, race them, or take the first success.
`yield return` / `for await` | You’re producing or consuming a stream of values — paginated APIs, card dealing, log tailing.
`defer` | You acquire resources at different points and want guaranteed cleanup without nested `try`/`finally`.
`AsyncChannel` | Two or more tasks need to communicate — producer/consumer, fan-out/fan-in, or hand-off.
`AsyncScope` | You want child task lifetimes tied to a scope with automatic cancellation on failure.
Framework adapters | You’re already using Reactor or RxJava and want `await` / `for await` to work with their types.

## 13. Quick Reference

Construct | Description
--- | ---
`async { …​ }` | Start a closure on a background thread. Returns `Awaitable` (or `Iterable` for generators).
`await expr` | Block until the result is available. Rethrows the original exception.
`await(a, b, c)` | Wait for all — syntactic sugar for `await Awaitable.all(a, b, c)`.
`yield return expr` | Produce a value from an async generator. Consumer blocks until ready.
`for await (x in src)` | Iterate over an async source (generator, channel, Flux, Observable, etc.).
`defer expr` | Schedule a cleanup action (LIFO order) inside an `async` closure.
`AsyncChannel.create(n)` | Create a buffered (or unbuffered) channel for inter-task communication.
`ChannelSelect.after(ms)` | Timer offer: a `ChannelSelect` branch that re-arms each round and commits with the `Instant` it fired.
`AsyncChannel.after(ms)` | Timer channel: delivers the `Instant` it fired, then closes. A fixed deadline as a `ChannelSelect` branch.
`AsyncScope.withScope { …​ }` | Structured concurrency — all children complete (or are cancelled) on scope exit.
`Awaitable.orTimeoutMillis` | Apply a deadline. Throws `TimeoutException` if the task exceeds it.
`Awaitable.completeOnTimeoutMillis` | Apply a deadline with a fallback value instead of throwing.
`Awaitable.delay(ms)` | Non-blocking pause.

All keywords (`async`, `await`, `defer`) are contextual — they can still be used as variable or method names in existing code.

## 14. Monadic comprehensions (Incubating)

The `groovy-macro-library` module provides `DO`, a comprehension macro that rewrites a sequence of `name in source` generators followed by a body into a chain of bind operations on the carrier type. It gives Scala-style for-comprehension / Haskell-style do-notation ergonomics to any type with monadic shape — `Optional`, `Stream`, `CompletableFuture`, Groovy’s `Awaitable` and `DataflowVariable`, and user-defined types that opt in.

| `DO` is incubating (since Groovy 6.0) and may change. The macro is compile-time only; the generated code calls the `Comprehensions` runtime support in core, so a program using `DO` needs only the core `groovy` jar at runtime — `groovy-macro-library` is a compile-time dependency.

    def result = DO(a in Optional.of(2),
                    b in Optional.of(3)) {
        Optional.of(a + b)
    }
    assert result.get() == 5

### 14.1. Desugaring

Every generator becomes a bind; the body is the innermost closure body and must itself yield a value of the carrier type (the do-notation rule — there is no implicit lifting in this version). The example above expands to:

    Comprehensions.bind(Optional.of(2)) { a ->
        Comprehensions.bind(Optional.of(3)) { b ->
            Optional.of(a + b)
        }
    }

Because the macro expands before type information is available, it does not emit a carrier-specific method name directly. `Comprehensions.bind` resolves the right operation at runtime; under `@CompileStatic` the `groovy.typecheckers.MonadicChecker` extension supplies the static types (see Static type checking).

| For all but the most trivial uses, `DO` under `@CompileStatic` or `@TypeChecked` requires the `MonadicChecker` extension (`@CompileStatic(extensions = 'groovy.typecheckers.MonadicChecker')`). The runtime dispatcher signature `Comprehensions.bind(Object, Closure):Object` erases the carrier’s element type and the comprehension’s result type, so without the extension each generator’s bound name and the whole `DO` expression both fall through to `Object` — which the static type checker will reject as soon as the body or downstream code does anything type-specific with either. See Static type checking.

A name bound by an earlier generator is in scope in the source expression of every later generator and in the body:

    def result = DO(a in Optional.of(10),
                    b in Optional.of(a * 2)) {   // b's source depends on a
        Optional.of(a + b)
    }
    assert result.get() == 30

Short-circuiting is delivered by the carrier, not by the macro: an empty or failed carrier simply propagates and the body is never evaluated.

    def result = DO(a in Optional.empty(),
                    b in Optional.of(3)) {
        Optional.of(b)            // never reached
    }
    assert result.isEmpty()

### 14.2. Participating carriers

A type participates as a carrier when, in order, the first match winning:

1. it is on the standard allow-list — `java.util.Optional`, `java.util.stream.Stream`, `java.util.concurrent.CompletionStage` (covering `CompletableFuture`), and `groovy.concurrent.Awaitable` (covering `DataflowVariable`);

2. it is structural — it has a single-argument `flatMap` (and, for the map role, `map`);

3. it is annotated `@groovy.transform.Monadic`, optionally declaring non-conventional method names.

The allow-list also recognises common Functional Java carriers by name — `fj.data.Option`, `fj.data.List`, `fj.data.Stream`, `fj.data.Validation` and `fj.P1` — using that library’s `bind`/`map` convention. Groovy takes no dependency on Functional Java; the names are matched reflectively, and the generator closure is coerced to `fj.F` automatically. `fj.data.Either` is not directly monadic in Functional Java (bind lives on its `.right()`/`.left()` projections) and is not a carrier; use a projection explicitly.

| `Awaitable` and `DataflowVariable` bind via `thenCompose`; their `then` method is the map operation, not bind. `DO` over `Awaitable` therefore composes asynchronous values without an imperative `await` at each step:

    import groovy.concurrent.Awaitable
    import static org.apache.groovy.runtime.async.AsyncSupport.await

    def total = DO(a in Awaitable.of(2),
                   b in Awaitable.of(40)) {
        Awaitable.of(a + b)
    }
    assert await(total) == 42

`Stream` yields the usual cartesian composition:

    import java.util.stream.Stream

    def pairs = DO(x in Stream.of(1, 2),
                   y in Stream.of('a', 'b')) {
        Stream.of("$x$y".toString())
    }
    assert pairs.toList() == ['1a', '1b', '2a', '2b']

A user type opts in with `@Monadic`, which may name a non-conventional bind and map method:

    import groovy.transform.Monadic
    import java.util.function.Function

    @Monadic(bind = 'chain', map = 'transform', unit = 'of')
    class Result {
        final Object value
        Result(Object value) { this.value = value }
        static Result of(value) { new Result(value) }     // unit: not used by DO, declared for law tooling
        Result chain(Function f) { (Result) f.apply(value) }
        Result transform(Function f) { new Result(f.apply(value)) }
        boolean equals(o) { o instanceof Result && value == o.value }   // results compare by value...
        int hashCode() { value == null ? 0 : value.hashCode() }
    }

    def r = DO(a in Result.of(3),
               b in Result.of(4)) {
        Result.of(a * b)
    }
    assert r == new Result(12)                            // ...so == means "same wrapped value"

The monad laws (left identity, right identity, associativity) are not enforced by the compiler; as with `@Reducer`/`@Associative`, lawful behaviour is the participating type’s responsibility. Because a comprehension composes monadic values, a data-style carrier should provide structural `equals`/`hashCode` (Groovy’s `==` dispatches to `equals`) — otherwise results compare by identity and laws such as right identity and functor identity silently fail to hold. This does not apply to effectful carriers like `Stream`, `CompletableFuture` and `Awaitable`, whose laws hold only up to observational equivalence. A carrier may optionally name its unit (`of`/`pure`) factory via `@Monadic(unit = '…​')`; `DO` does not use it — the body lifts explicitly — but it lets law-deriving tooling synthesize the unit-dependent laws (left and right identity).

### 14.3. Static type checking

Activate the `MonadicChecker` type-checking extension to use `DO` under `@CompileStatic` or `@TypeChecked`. It types each generator’s bound name as the carrier’s element type (so the body type-checks), restores the comprehension’s result type, and rejects a non-participating carrier with a compile error naming the type and the missing shape:

    import groovy.transform.CompileStatic

    @CompileStatic(extensions = 'groovy.typecheckers.MonadicChecker')
    class Calc {
        static int sum() {
            DO(a in Optional.of(2),
               b in Optional.of(3)) {
                Optional.of(a + b)
            }.get()
        }
    }
    assert Calc.sum() == 5

| The typical symptom of forgetting the extension is a static type checking error reporting that some operation cannot be found on `Object` — either inside the body (a generator’s bound name has erased to `Object`) or on the `DO` expression itself (the result has erased to `Object`). Adding `extensions = 'groovy.typecheckers.MonadicChecker'` to the `@CompileStatic`/`@TypeChecked` annotation resolves it.

#### 14.3.1. Lint for hand-written chains: MonadicShapeChecker

If you write `flatMap` / `map` chains by hand (without `DO`) over the same participating carriers, the sister `MonadicShapeChecker` extension provides a compile-time lint:

    @TypeChecked(extensions = 'groovy.typecheckers.MonadicShapeChecker')

It catches the two classic shape mistakes — a `bind` (i.e. `flatMap`) closure that returns a non-carrier value, and a `map` closure that returns the same carrier where `flatMap` was meant. A `strict` option tightens what is flagged; the default lenient mode reports only high-confidence violations. Both checkers are documented from the type-checker side in the Built-in auxiliary type checkers chapter.

### 14.4. When to use `DO`

`DO` is a value-composition notation. It complements, rather than competes with, the concurrency constructs: use imperative `async`/`await` when code reads as a sequence of dependent steps you intend to run now (see Prefer returning values over mutating shared state and Choosing the right tool); reach for `DO` when the composed value is the deliverable — an `Awaitable` to combine further, an `Optional`/validation result, a parser result, or a custom `Result` type — and you want one uniform notation across carriers.

| This version is deliberately narrow. The body must yield a carrier value (no implicit `pure`/`unit` lifting); there are no `if` guard clauses; and each `DO` works over a single carrier (nest `DO` blocks for more). `break`/`continue` are not valid in the body, and `return` follows the standard closure rule. These are the same closure-body constraints the `@Parallel` for-loop transform documents.