This guide takes you from an empty mix.exs to your first wait — in application code and in a
test — in a few minutes. It assumes no prior knowledge of WaitForIt.
For the complete API reference, see the WaitForIt module.
Installation
Add wait_for_it to your dependencies in mix.exs:
def deps do
[
{:wait_for_it, "~> 2.6"}
]
endThen run mix deps.get. WaitForIt's only dependency is :telemetry. Its application starts a
small supervision tree used by signal-based waiting, so there is nothing for you to add to your
own supervision tree.
The waiting forms are macros, so each module that uses them needs require WaitForIt (to call
them as WaitForIt.wait(...)) or import WaitForIt (to call them bare). The examples below
assume import WaitForIt.
One optional step, worth doing now: the macros read best without parentheses, and WaitForIt
exports formatter rules that keep mix format from adding them. Add it to import_deps in your
.formatter.exs:
[
import_deps: [:wait_for_it],
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]The big idea
Asynchronous work finishes when it finishes. Code that needs the result has three bad options and one good one.
The bad ones are familiar. Process.sleep(500) guesses: too short and it is flaky, too long and
every run pays for the worst case. A hand-rolled recursive polling loop works, but it is ten lines
of plumbing that says nothing about what you are waiting for. Restructuring everything around
messages is often right, but not always available — you may not control the other side at all.
The good option is to say what you are waiting for and let the library handle the rest:
# Wait until the record shows up, then bind it. Gives up after 2 seconds.
{:ok, user} = match_wait({:ok, %User{}}, Repo.fetch(User, id), timeout: 2_000)That returns as soon as the condition holds — not a fixed 500 ms later — and stops after 2 seconds if it never does.
The design idea that makes the rest of the library predictable: every waiting form mirrors an
Elixir control-flow construct you already know. wait/2 is a polling if, case_wait/3 is a
polling case, cond_wait/2 a polling cond, with_wait/3 a polling with. You already know
how they are spelled and what they do; the only new thing is that they keep looking.
Your first wait
wait/2 is the simplest form. It re-evaluates an expression until it is truthy, and returns
whatever ended the wait:
import WaitForIt
# Re-evaluates every 100ms for up to 5 seconds (the defaults).
if wait(File.exists?("data.csv")) do
IO.puts(File.read!("data.csv"))
else
IO.warn("gave up waiting for data.csv")
endOn success wait/2 returns the truthy value; on timeout it returns the last falsy value it saw —
which is what lets it drop straight into an if.
Two options control the loop, and every form accepts them:
wait(Job.finished?(id), timeout: :timer.seconds(30), interval: 250):timeout— how long to keep trying before giving up. Default5_000ms.:interval— how long to pause between attempts. Default100ms.
That is enough to be useful. Everything below is about picking a better-fitting form.
Choosing a form
You picked wait/2 above because "is it truthy yet?" was the whole question. When the question is
shaped differently, a different form says it more directly:
| You want to… | Use | Which mirrors |
|---|---|---|
| know whether something became truthy | wait/2 | truthiness, an if |
| pull a value out of a tagged result | match_wait/3 | = |
| branch on which of several outcomes arrived | case_wait/3 | case |
| wait for any one of several conditions | cond_wait/2 | cond |
| chain several waits, each depending on the last | with_wait/3 | with |
| build the condition at runtime, not compile time | until/2 | (a plain function) |
match_wait/3 is the one you will reach for most in practice, because most Elixir functions
answer with {:ok, value} or {:error, reason}:
{:ok, order} = match_wait({:ok, %Order{}}, Repo.fetch(Order, id), timeout: 2_000)And case_wait/3 when more than one outcome is interesting:
case_wait Repo.reload(job).status, timeout: :timer.seconds(30), interval: 250 do
:completed -> {:ok, job}
:failed -> {:error, :job_failed}
else
status -> {:error, {:timeout, status}}
endNote the else block. It runs only on timeout, and can match on the last value seen. Which
brings us to the one rule worth learning.
The one rule about timeouts
On timeout, each form behaves exactly as its built-in Elixir counterpart would on a final evaluation in which nothing matched.
A case_wait that gives up raises CaseClauseError, because that is what a case does when no
clause matches. A match_wait that gives up raises MatchError, because that is what = does.
A with_wait returns the last unmatched value, because that is what with does.
There is nothing WaitForIt-specific to memorize here — if you know the native construct, you already know the waiting one. Two escape hatches sit on top:
- an
elseblock turns a timeout into a value instead of an error, and - a
!variant of every form (wait!/2,match_wait!/3, …) raises a uniformWaitForIt.TimeoutErrorinstead, carrying the wait type, the timeout, and the last value.
# Raises WaitForIt.TimeoutError, with the last value seen, if the order never arrives.
order = match_wait!({:ok, %Order{}}, Repo.fetch(Order, id), timeout: 2_000)Your first wait in a test
Tests are where most people meet this library, and they get a dedicated module. WaitForIt.Test
provides ExUnit assertions that wait, and that fail like any other ExUnit assertion — with the
source expression and the last value seen — rather than raising a library-specific error:
defmodule MyApp.CheckoutTest do
use ExUnit.Case
use WaitForIt.Test
test "the order is eventually fulfilled" do
start_checkout(order_id)
assert_eventually {:ok, %Order{status: :fulfilled}} = Repo.fetch(Order, order_id)
end
endThree assertions are available, with timeouts already tuned for tests:
assert_eventually/2— becomes truthy, or matches, within the window (default 1 s).refute_eventually/2— never becomes truthy within the window (default 100 ms).assert_always/2— stays truthy for the whole window (default 100 ms).
Reach for these before the raw macros in tests. The Waiting in tests guide covers both, and the traps worth knowing.
When polling is the wrong tool
Everything so far polls: WaitForIt re-runs your expression on a timer. That needs no cooperation from the code you are waiting on, which is exactly why it is the default.
When you do control that code, you can do better. Pass a :signal and the waiter blocks on its
mailbox instead of a timer, waking only when the other side says something changed:
# CONSUMER — parks until signaled, then re-checks.
wait(Buffer.count() >= 4, signal: :buffer_filled)
# PRODUCER — after doing something that might satisfy waiters.
Buffer.put(item)
WaitForIt.signal(:buffer_filled)No polling latency, no wasted evaluations. See Polling vs signaling.
One thing to avoid
Do not put a catch-all clause in a case_wait/3 or cond_wait/2:
case_wait Repo.reload(job).status do
:completed -> {:ok, job}
_ -> {:error, :unknown} # ❌ matches on the very first evaluation — this never waits
endA clause that always matches ends the wait immediately. That is what else is for: it runs only
on timeout. The same trap has a quieter version — a bare variable pattern in match_wait/3
matches anything too. Both are covered in Waiting in tests.
Where to go next
- Waiting in tests — the assertions in depth, and using the macros directly when you want their exact return values.
- Polling vs signaling — the two modes, and choosing between them.
- Composing waits — chaining several waits with
with_wait/3. - Recipes — patterns for databases, processes, HTTP endpoints, and mailboxes.
- Telemetry — observing waits in production.
- Cheatsheet — every form, option, and default on one page.
Next: Waiting in tests