All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog and this project adheres to Semantic Versioning.
2.6.0 - 2026-09-18
Added
The
WaitForIt.Testassertions now tag their telemetry with the construct that issued the wait, aswait_context: %{construct: :assert_eventually | :refute_eventually | :assert_always}— matching whatwith_wait/3has done for its<~clauses since 2.4.0.refute_eventually/2andassert_always/2succeed by timing out: confirming a negative means waiting out the whole window. Nothing in[:wait_for_it, :wait, :stop]distinguished that from a wait that genuinely gave up, so a handler reporting onresult: :timeout— the use the Telemetry section of the README suggests — reported every passing assertion as a failure. In a suite converted to the test assertions, an unfiltered handler produced 7 rows across two files, 6 of which were passing assertions and 1 a real signal; with the tag, 1 row.Additive and backward compatible: a
:wait_contextpassed explicitly still wins, and handlers that ignore the key are unaffected. The Telemetry guide, theWaitForIt.Testmoduledoc andusage-rules.mdall now spell out that a passingrefute_eventually/assert_alwaysemitsresult: :timeout, and show the exclusion a timeout-reporting handler needs.Two traps in
usage-rules.mdthat a large adoption hit repeatedly:A bare variable pattern never waits.
match_wait/3, a<~clause, andassert_eventually/2'spattern = expressionform all test the pattern before binding it, and a bare variable matches anything — so the wait halts on its first evaluation and hands back whatever it saw, usually thenilit was meant to wait for the expression to stop being. It is the same mistake as a catch-all clause incase_wait/3, in the one place the compiler cannot warn about it. This bites hardest on thewhereis-style functions that returnpid | nil, which is exactly where a<~clause ormatch_waitlooks most natural.refute_eventually/2means never, not "eventually not". Waiting for something to become false — a metric that gets cleaned up, a process that gets reaped — isassert_eventually(not x).A Getting started guide, which is now the HexDocs landing page. Installation was the one thing the published documentation did not have: the
## Installationsection sat below the<!-- README END -->marker, so it was excluded from theWaitForItmoduledoc — which was the landing page. A reader arriving at hexdocs.pm/wait_for_it was shown the five waiting forms and the timeout rules without ever being told the dependency line. The new guide runs frommix deps.getthrough a first wait, choosing a form, the one timeout rule, and a first test assertion. Installation stays out of theWaitForItmoduledoc, which is reference material for readers who already have the dependency.A Cheatsheet, collecting every form, option, default, telemetry event and trap on one page, for lookup rather than reading.
An About guide — the ideas the library is built on (borrowed syntax, the one timeout rule, polling versus signaling) and how it got from the 2017 release to here.
A WaitForIt and AI coding agents guide, and
usage-rules.mdis now published on HexDocs. The rules have shipped in the package since 2.5.0, but nothing explained how to wire them into a project, and a reader could not see what their agent was being told without digging intodeps/. Both are fixed: the guide coversusage_rulessetup and the manual route, and the rules themselves are an extra under Reference..formatter.exsnow ships in the package. It has always exportedlocals_without_parensfor all of the waiting macros, but it was not in the:fileslist, so the export block was invisible to everyone installing from Hex:import_deps: [:wait_for_it]found nothing andmix formatre-parenthesised every wait in a downstream project.A Changelog link in the hex.pm package metadata, alongside the GitHub link.
Changed
- The documentation is organised into Guides, Reference and About, replacing a single regex-matched Guides group, with the guides ordered as a reading path — Getting started, Waiting in tests, Polling vs signaling, Composing waits, Recipes, Telemetry, Troubleshooting, AI coding agents — and the Cheatsheet and usage rules kept out of that path, since they are for lookup.
Fixed
An unparenthesised guard on a
<~clause now raises aArgumentErrornaming the fix.whenbinds looser than<~, sowith_wait on(p when is_pid(p) <~ whereis(id)) doparses as a
<~nested inside the guard rather than a guarded pattern.with/1then reported it as three separate "undefined variable p" errors — pointing at the pattern, the guard, and thedoblock — none of which mention parentheses.with_wait/3now detects the shape at expansion and says to write(p when is_pid(p)) <~ ….Not an exotic case: a
<~clause on a function returningpid | nilneeds a guard, for the bare-variable reason above.The
WaitForIt.Backoffexample inusage-rules.mddid not work. It readexponential(cap: 2_000, jitter: true)::capis not an option — the option is:max— and was silently ignored, whilejitter: trueraisesArithmeticErroron the first computed delay, becauseapply_jitter/2'sjitter <= 0.0guard is false for an atom and the fallback clause multiplies by it. TheWaitForIt.Backoffmoduledoc was correct throughout; only the agent usage rules were wrong, which is the file coding agents are pointed at.Documentation links on the hex.pm package page went to raw Markdown rather than HexDocs. hex.pm renders the README with every relative link rewritten to the package tarball preview, so "Waiting in tests" landed the reader on
https://repo.hex.pm/preview/wait_for_it/2.5.0/guides/waiting_in_tests.md— unrendered Markdown — instead of the guide. Guide links in the README are now absolute HexDocs URLs, which hex.pm leaves alone, and theWaitForItmoduledoc strips the prefix back off as it slices the README, so ExDoc still resolves them within the version being browsed and still warns if an extra is missing. (Issue #31)The guide-to-guide navigation chain dead-ended at Telemetry, which closed with "That's the end of the guides" even though Troubleshooting followed it in the sidebar — and Troubleshooting had no navigation footer at all. Every guide now links to the next one.
2.5.0 - 2026-08-25
Added
Elixir 1.19 and 1.20 to the CI matrix, which now spans the declared floor (
~> 1.15) through the current release. The two most recent Elixir versions — the ones most adopters run — were previously untested, and the set-theoretic type checker is exactly the kind of moving target a macro-heavy library needs coverage against. Thelintflag moved to the newest entry so formatting and Credo run against current tooling, and the Dialyzer job now matches.tool-versionsinstead of trailing the matrix by two releases. (Issue #23)A Troubleshooting guide, opening with "Why does the compiler say my pattern will never match?" (Issue #24)
usage-rules.md, shipped in the package — WaitForIt's guidance condensed for AI coding agents, in the layoutusage_rulessyncs from. A consumer can pull it into theirAGENTS.mdwithmix usage_rules.sync, or read it directly atdeps/wait_for_it/usage-rules.md.It leads with the one rule — on timeout each form behaves as its native counterpart would on a final non-matching evaluation — and then the traps that are easy to get wrong and silent when you do: a catch-all clause in
case_wait/cond_waitdisables the waiting entirely (measured: it halts after one evaluation in 4 ms), waiting blocks the calling process and must not happen inside a GenServer callback, signals are node-local, andtimeout: :infinityremoves all timeout behaviour rather than merely extending it.
Changed
Telemetry metadata
envis now a trimmed map rather than the caller's wholeMacro.Env(Issue #22). It carries exactly:context,:context_modules,:file,:function,:line, and:module— the same six fieldsWaitForIt.TimeoutErrorhas always exposed. The two paths disagreed about whatenvmeant; now they share one definition and cannot drift apart again.A full
__ENV__measured 1019 words (~8 KB) in a module with ordinary imports, of which most is the calling module's import table (:functions,:macros,:requires) — it grows with the caller's imports and is of no use to a handler. That term was embedded in the caller's compiled module once per wait, copied into the signal registry's ETS on every signal-based wait, and copied again by every handler that forwarded metadata off-process.The trim happens at macro expansion, so the fat term is never emitted rather than being discarded later. Measured on a module with three wait sites:
before after envterm1019 words 29 words whole event metadata 1039 words 49 words caller's compiled beam 12,612 bytes 3,360 bytes A handler reading anything outside those six keys needs updating; one reading
env.fileorenv.linedoes not. The value passed down internally stays a real%Macro.Env{}so thatMacro.Env.stacktrace/1still reraises a timed-outmatch_wait/case_wait/cond_waitat the caller's location — byte-for-byte the stacktrace it produced before.
Fixed
The test suite no longer emits "the following clause will never match" warnings on Elixir 1.20. These came from the suite's own stub helpers, not from the waiting macros: 1.20 infers an exact return type for a local function, so
defp pending, do: :pendinghas the type:pending, and a wait for{:ok, _}on it genuinely cannot match. The helpers wanted a runtime non-match to drive the timeout paths, not a type-level one, so they now route through the process dictionary — same value, no inferable type.Characterised across waitable shapes before changing anything: the warning fires only when the expression's inferred type is disjoint from the pattern. A union that includes the pattern, an opaque value, a
@spec'dterm(), thenil | structshape ofRepo.get/2, and a tagged{:ok, _} | {:error, _}union are all clean, as arewait/2andcase_wait/3. So no realistic waitable trips it, and when it does fire it is correct — waiting changes values over time, not types, and an expression whose type cannot produce the pattern can only ever time out.WaitForIt therefore does not suppress the diagnostic in its expansion. The mechanism that produces the surprising warning is the same one that catches a genuinely impossible pattern in user code; silencing it library-wide would trade a rare accurate warning for a permanent blind spot. The guide explains the diagnosis and the fix instead. (Issue #24)
2.4.0 - 2026-07-31
Added
- The
:timeoutoption now accepts:infinity, for a wait that continues until its condition is met rather than giving up after a fixed budget. Such a wait can never time out, so the!variants never raise,elseclauses never run, andWaitForIt.until/2never returns{:timeout, last_value}. (Issue #6) - Telemetry metadata now includes a
wait_contextkey, which distinguishes a wait that was written directly (nil) from one a construct desugared to. A<~clause of awith_waitreports%{construct: :with_wait, clause: index}, so its events are no longer indistinguishable from a standalonematch_waitand can be attributed to a specific clause. (Issue #19)
Fixed
- Corrected the
WaitForIt.wait_opt/0typespec, which declared:interval(and its:frequencyalias) as an integer only, even though aWaitForIt.Backofffunction has been accepted since 2.2.0.
2.3.0 - 2026-07-31
Added
- Added a functional (non-macro) waiting API,
WaitForIt.until/2andWaitForIt.until!/2, for conditions that are computed at runtime or passed in as a function.until/2returns a tagged{:ok, value}or{:timeout, last_value}result;until!/2returns the bare value on success and raisesWaitForIt.TimeoutErroron timeout.
Changed
- Clarified the "Timeout behavior" documentation to lead with the single underlying rule — on timeout, each waiting form behaves exactly as its native Elixir counterpart would on a final non-matching evaluation — and demoted the behavior matrix to a reference table (now with a native-counterpart column). Documentation only; no API or behavioral changes.
2.2.1 - 2026-06-18
Changed
- Restructured the documentation so the README is the single source for the
WaitForItmodule documentation, removing the duplicated Overview page. Documentation only; no API or behavioral changes.
2.2.0 - 2026-06-18
Added
- Added the
with_wait/3andwith_wait!/3macros for composing several waits in awith-style pipeline. Clauses use<-(ordinary one-shot match) or<~(wait-for-match, with optional per-clause options); a<~timeout flows to theelseblock like an ordinary non-match (or raisesWaitForIt.TimeoutErrorforwith_wait!). See the "Composing waits" guide. - Added the
WaitForIt.Testmodule withassert_eventually/2(truthy and pattern-binding forms),refute_eventually/2, andassert_always/2test assertions that fail with a regularExUnit.AssertionError(including the source expression and last value) on timeout. - Documented and promoted the
match_wait/3construct, and added amatch_wait!/3bang variant. - Added the
:intervaloption as the preferred name for the polling interval.:frequencycontinues to work as an alias and is slated for removal in a future major version. Added
:telemetryevents ([:wait_for_it, :wait, :start | :stop | :exception]) for every wait, exposing wait duration, evaluation count, and outcome.- Added backoff support: the
:intervaloption now accepts a 1-arity function of the attempt number, plus a newWaitForIt.Backoffmodule withconstant/1andexponential/1strategies. - Added guides (Waiting in tests, Polling vs signaling, Composing waits, Recipes, Telemetry) and a rewritten README.
- Added GitHub Actions CI (test matrix, formatting, Credo, Dialyzer).
Changed
- Rewrote the internal wait loop to use a single monotonic deadline, making timeouts immune to wall-clock adjustments and unifying the polling and signaling code paths (the polling mode no longer spawns a helper process per wait).
- Deprecated the
WaitForIt.V1macros; they now emit deprecation warnings pointing at the currentWaitForItAPI and will be removed in a future major version. - Modernized dependencies (
ex_doc,stream_data,credo).
2.1.0 - 2023--11-14
Changed
- Further improved documentation.
WaitForIt.case_wait/3will now raise aCaseClauseErroron timeout if there is noelseblock.WaitForIt.cond_wait/2will now raise aCondClauseErroron timeout if there is noelseblock.
2.0.0 - 2023-11-02
Changed
- Much improved documentation.
- Breaking change to return value of
WaitForIt.wait/2,WaitForIt.case_wait/3, andWaitForIt.cond_wait/2. - Rewrite of WaitForIt internals.
- Moved legacy code to
WaitForIt.V1.
1.4.0 - 2023-10-24
Added
- Add WaitForIt.wait! macro.
[1.3.0] - 2020-04-02
Changed
- Use DynamicSupervisor to manage condition variables.
[1.2.1] - 2019-03-14
Added
- Add
:pre_waitoption to all forms of waiting.
[1.2.0] - 2019-03-08
Added
- Add support for match clauses in
elseblock ofcase_wait. (Issue #9)
1.1.1 - 2018-03-03
Added
- Add idle timeout feature for ConditionVariable.
1.1.0 - 2017-09-02
Added
- Add support for
elseclause incase_waitandcond_wait. (Issue #4) - Add this CHANGELOG
Changed
- Use supervisor to manage condition variables. (Issue #5)
Fixed
- Grammar fixes for README and @moduledoc. Thanks to @GregMefford for the fixes.
- Fix unexpected messages from wait_for_it when used with Genserver
1.0.0 - 2017-08-28
- Initial release supporting
wait,case_wait, andcond_waitwith either polling or condition variable signaling.