# Composing waits
A single wait answers one question: "has *this* become true yet?" Real workflows often need to
chain several such questions, where each step depends on the previous one and any step might fail
or time out. `WaitForIt.with_wait/3` composes waits into a single `with`-style pipeline.
## The shape
```elixir
with_wait on(
{:ok, token} <- authenticate(user), # one-shot match (no waiting)
{:ok, account} <~ {load_account(token), timeout: 2_000}, # wait, with per-clause options
{:ok, balance} <~ fetch_balance(account) # wait, with global/default options
), interval: 50 do
{:ok, balance}
else
{:error, reason} -> {:error, reason}
still_pending -> {:still_waiting, still_pending}
end
```
It reads like `with/1`, with two clause arrows:
- **`<-`** — an ordinary `with` clause: evaluated **once**. Matches → bind and continue; doesn't
match → route to `else`.
- **`<~`** — a **wait-for-match** clause: re-evaluate the right-hand side until it matches the
pattern, then bind and continue.
The clauses are wrapped in `on(...)` (a purely syntactic wrapper — there is no `on` function), and
global options for every `<~` clause go between the wrapper and the block.
## How failure and timeout flow
`with_wait` mirrors `with/1`:
- If every clause matches (waiting as needed), the `do` block runs and its value is the result.
- If a `<-` clause doesn't match, control goes to `else` with the non-matching value.
- If a `<~` clause **times out**, its last evaluated value flows to `else` too — a timeout is
treated exactly like a non-match. With no `else`, that last value becomes the result.
This is what makes composition clean: a stalled step and a wrong-shaped step are handled the same
way, in one place.
```elixir
# If load_account/1 never returns {:ok, _} within 2s, `:still_loading` (its last value) flows
# to the else block.
with_wait on({:ok, account} <~ {load_account(token), timeout: 2_000}) do
account
else
:still_loading -> {:error, :account_timeout}
end
```
## Options
Every `<~` clause accepts the usual options — `:timeout`, `:interval` (including a
`WaitForIt.Backoff` function), `:pre_wait`, and `:signal`. Per-clause options override the global
options:
```elixir
with_wait on(
{:ok, a} <~ slow_thing(), # uses the global timeout: 5_000
{:ok, b} <~ {flaky_thing(), timeout: 500} # this one gives up after 500ms
), timeout: 5_000 do
{a, b}
end
```
## Guards and the `<~` precedence rule
`<~` binds more tightly than `when` and the comparison operators, so guards and comparison-heavy
right-hand sides must be parenthesized:
```elixir
# Wait until a counter exceeds a threshold:
with_wait on(({:ok, n} when n > 5) <~ read_counter()) do
n
end
# Parenthesize a comparison on the right-hand side:
with_wait on(found <~ (Enum.find(items, &ready?/1) != nil)) do
found
end
```
Simple clauses such as `{:ok, x} <~ fetch(id)` and `{:ok, x} <~ {fetch(id), timeout: 100}` need no
parentheses. If a wait is dominated by a single complex condition, `case_wait/3` is often clearer.
## Raising instead of routing: `with_wait!`
`with_wait!/3` is identical except that a `<~` clause that times out raises a
`WaitForIt.TimeoutError`. An `else` block is still honored for ordinary `<-` non-matches.
```elixir
{:ok, balance} =
with_wait! on(
{:ok, account} <~ load_account(token),
{:ok, balance} <~ fetch_balance(account)
), timeout: 2_000 do
{:ok, balance}
end
```
## Observability
Each `<~` clause runs as its own wait, so it emits the standard
`[:wait_for_it, :wait, :start | :stop | :exception]` telemetry events. See the
[Telemetry guide](telemetry.md).
---
**Previous:** [Polling vs signaling](polling_vs_signaling.md) · **Next:** [Recipes](recipes.md)