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"}
  ]
end

Then 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")
end

On 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. Default 5_000 ms.
  • :interval — how long to pause between attempts. Default 100 ms.

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…UseWhich mirrors
know whether something became truthywait/2truthiness, an if
pull a value out of a tagged resultmatch_wait/3=
branch on which of several outcomes arrivedcase_wait/3case
wait for any one of several conditionscond_wait/2cond
chain several waits, each depending on the lastwith_wait/3with
build the condition at runtime, not compile timeuntil/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}}
end

Note 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 else block turns a timeout into a value instead of an error, and
  • a ! variant of every form (wait!/2, match_wait!/3, …) raises a uniform WaitForIt.TimeoutError instead, 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
end

Three 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
end

A 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