Errors & Diagnosis
xa11y errors are designed so you never have to re-run a failure under extra logging to understand it. A timeout tells you what it was waiting for and what it last saw; a selector miss tells you what was actually there. The same information is available two ways:
- Rendered into the message. The exception text alone is enough to diagnose a CI failure from the log.
- As structured fields. Exception attributes in Python, error
properties in JavaScript, and the
Diagnosisstruct in Rust, so harnesses can act on them programmatically instead of parsing prose.
Anatomy of a timeout
Section titled “Anatomy of a timeout”A failed wait_visible(timeout=60) renders like this:
xa11y.TimeoutError: Timeout after 60.0s; waiting for: visible; selector:dialog[name^="Submit to AWS Deadline Cloud"]; last observed: selector nevermatched; candidates: window "Untitled — Cinema 4D"search scope (bounded):application "Cinema 4D" (pid=4242)application "Finder" (pid=512)…Reading it back:
waiting fornames the wait condition. Thewait_*methods reportvisible,attached,enabled, and so on. An auto-waiting action reportspress target actionable (visible && enabled), an event wait reportsevent matching predicate, andwait_untilreportscustom predicate.selectoris the full selector the locator was resolving.last observedis the final poll’s observation, which is what separates the two failure modes when debugging:selector never matchedmeans the element was never there (wrong selector, wrong window, app not ready).matched button "Export" (visible=false, enabled=true, focused=false)means the element was there in the wrong state. No candidate or scope sweep is attached in this case. The states already tell the story.selector matched 2 element(s); nth(5) requestedmeans annth()index out of range.
candidateslists near-misses: elements with the selector’s target role but different attributes. A typo likebutton[name="Exprot"]listsbutton "Export"right in the error.search scope (bounded)is a depth-limited, line-capped snapshot of where the search happened: a tree dump for scoped locators, the running application list for rootless ones and app lookups. This replaces theprint(app.dump())wrapper you’d otherwise write around the failing call.
Selector misses from fail-fast calls (element(), tree(), dump())
carry the same context on SelectorNotMatchedError.
Diagnosis collection is bounded and failure-path-only: candidate lists and snapshots are truncated (a miss against a huge tree cannot produce a megabyte message), and nothing is collected unless the operation has already failed.
Structured access
Section titled “Structured access”import xa11y
try: dialog.wait_visible(timeout=60)except xa11y.TimeoutError as e: e.condition # "visible" e.selector # 'dialog[name^="Submit"]' e.last_observed # 'selector never matched' e.candidates # ['window "Untitled — Cinema 4D"', ...] e.scope # bounded tree dump / app list (str) e.elapsed # 60.0 (seconds, float)Every attribute is always present (None / [] when not applicable),
so no hasattr guards are needed. SelectorNotMatchedError exposes
the same fields (elapsed is always None there).
const { TimeoutError } = require('xa11y');
try { await dialog.waitVisible(60);} catch (e) { if (e instanceof TimeoutError) { e.condition; // 'visible' e.selector; // 'dialog[name^="Submit"]' e.lastObserved; // 'selector never matched' e.candidates; // ['window "Untitled — Cinema 4D"', ...] e.scope; // bounded tree dump / app list (string) e.elapsedMs; // 60000 }}use std::time::Duration;use xa11y::{Diagnosis, Error};
match dialog.wait_visible(Duration::from_secs(60)) { Err(e @ Error::Timeout { .. }) => { if let Some(d) = e.diagnosis() { let _ = (&d.condition, &d.selector, &d.last_observed, &d.candidates, &d.scope); } } other => { /* ... */ }}App lookups
Section titled “App lookups”App.by_pid / App.by_name / App.find timeouts report a bounded list of
the apps that were running, so an attach failure on an opaque CI runner is
diagnosable from the message:
xa11y.SelectorNotMatchedError: No element matched selector:application[pid=4242]; waiting for: application discovery; last observed:enumerated 12 running apps, 1 without a resolvable pid; candidates:"Finder" (pid=512), "Terminal" (pid=890), …Event waits
Section titled “Event waits”Subscription.wait_for timeouts report how many non-matching events
arrived, and distinguish a quiet stream from a disconnected one:
Timeout after 5.0s; waiting for: event matching predicate; last observed:event source disconnected after 3 non-matching event(s) — no furtherevents will arriveFor contributors
Section titled “For contributors”This behavior is governed by design tenet 6 (errors carry their own
diagnosis) in AGENTS.md. The structured carrier is the Diagnosis
struct in xa11y-core/src/error.rs. Its module docs describe the pattern:
enrich at the terminal failure site, keep poll-loop retry signals cheap,
bound every payload, and never let diagnosis collection mask the original
error. New failure paths should attach a Diagnosis rather than
interpolating context into ad-hoc message strings.