Ego‑Browser Element Resolution Transience Rules: When to Retry vs. Fail
Ego‑Browser treats element resolution failures as either transient (retryable) or permanent based on whether the condition can realistically improve with time—unknown references, missing box models, and unmatched locators are transient, while ambiguous selectors and out‑of‑range indices are permanent.
Ego‑Browser, a lightweight browser automation driver from the citrolabs/ego-lite repository, implements a unified resolution system that distinguishes between temporary and unrecoverable lookup failures. Understanding these element resolution transience rules is essential for writing reliable automation scripts that handle dynamic page states correctly.
How Element Resolution Errors Are Classified
At the core of the system is ElementResolutionError, defined in src/element-resolver.ts. Every resolution helper—including resolveElementCenter, resolveElementObjectId, and resolveLocatorCenter—throws this error with a kind property set to either "transient" or "permanent".
The classification determines behavior in higher‑level wait utilities. In src/driver/waits.ts, the generic wait loop checks err.kind === "transient" before deciding whether to continue polling or abort immediately.
Transient Failures: Retryable Conditions
These situations indicate the element may appear later and warrant continued polling:
-
Unknown reference IDs – When a ref lookup fails because the backend node became stale or the ID is not yet registered. The code explicitly throws:
new ElementResolutionError(\Unknown ref: ${refId}`, "transient")`. -
Missing or degenerate box model – If
DOM.getBoxModelreturns no usable geometry data, the element might not be rendered yet (hidden, loading, or outside viewport). The error message includes"Element has no box model"with transient classification. -
Locator resolves to no element – When
Runtime.evaluateor a selector query returnsnull, the dynamic content may still be loading. The error reads:new ElementResolutionError(\Element not found: ${selectorOrRef}`, "transient")`. -
Zero element matches – Identical logic applies when
count === 0; the selector could match after DOM updates. -
Role‑based lookup with no match – If
findBackendNodeIdsByRoleNamereturns empty results, the role/name pair may be added post‑navigation or during hydration.
Permanent Failures: Non‑Retryable Conditions
These situations represent logic errors that waiting cannot fix:
-
Ambiguous locators without explicit index – When a selector matches multiple elements and no
nthparameter is provided. The error is thrown withkind: "permanent"because retrying will never resolve the ambiguity. -
Out‑of‑range index with explicit
nth– If you specify an index but the match count is insufficient, that index will never become valid. The selector itself is faulty. -
Multiple role matches when uniqueness is required – When role‑based lookup finds several candidates but the caller expects exactly one, the ambiguity is permanent.
-
Malformed selectors or other explicit permanent errors – Any path that constructs
ElementResolutionErrorwith"permanent"directly, such as syntactically invalid CSS selectors.
The matchCountKind helper in src/element-resolver.ts centralizes this logic: counts greater than one map to permanent failures, while zero or one map to transient.
Implementation in Wait Utilities
The transience rules integrate with Ego‑Browser's polling mechanisms through src/driver/waits.ts. The generic wait loop uses the error kind to control flow:
// Simplified pattern from src/driver/waits.ts
await waitFor(async () => {
try {
return await resolveElementCenter(cdp, sessionId, refMap, selector)
} catch (err) {
if (err instanceof ElementResolutionError && err.kind === 'transient') {
// Continue polling – condition may improve
throw err
}
// Permanent error – abort to avoid infinite wait
throw new AbortError(err.message)
}
})
This pattern appears throughout waitForSelector, waitForRef, and related utilities.
Practical Code Examples
Handling Dynamic Content with Transient Retries
// Example: waiting for an element that may appear later
await waitFor(async () => {
try {
const { x, y } = await resolveElementCenter(cdp, sessionId, refMap, 'css:#dynamic-button')
return { x, y }
} catch (err) {
if (err instanceof ElementResolutionError && err.kind === 'transient') {
// Retry – element not ready yet
throw err
}
// Permanent error – abort the wait
throw new AbortError(err.message)
}
})
Detecting Ambiguous Selectors (Permanent Failure)
// Example: a locator that is ambiguous (permanent failure)
try {
await resolveLocatorObjectId(cdp, sessionId, { kind: 'css', selector: '.item' })
} catch (err) {
if (err.kind === 'permanent') {
console.error('Selector matches multiple elements – make it more specific')
}
}
Role‑Based Lookup with Retry Semantics
// Example: role‑based lookup that retries until the element appears
try {
const obj = await resolveElementObjectId(cdp, sessionId, refMap, 'role:button[name="Submit"]')
// use obj.objectId …
} catch (err) {
if (err.kind === 'transient') {
// element not yet present – the wait loop will retry
}
}
Key Source Files
Understanding the transience rules requires familiarity with three modules:
| File | Responsibility |
|---|---|
src/element-resolver.ts |
Defines ElementResolutionError, implements matchCountKind, and encodes transient/permanent decisions for all resolution scenarios |
src/driver/locator.ts |
Wraps element lookups and propagates error kinds to calling code |
src/driver/waits.ts |
Implements polling loops that respect err.kind to determine retry behavior |
Summary
-
Transient errors (
"transient") indicate the element may appear after navigation, rendering, or DOM updates—Ego‑Browser continues polling in wait utilities. -
Permanent errors (
"permanent") indicate selector ambiguity, impossible indices, or malformed queries—Ego‑Browser aborts immediately because the condition cannot self‑resolve. -
The classification originates in
src/element-resolver.tsviaElementResolutionError, propagates throughsrc/driver/locator.ts, and controls flow insrc/driver/waits.ts. -
Design selectors to be unambiguous; use explicit
nthparameters or more specific criteria to avoid permanent failures on otherwise valid elements.
Frequently Asked Questions
How do I force Ego‑Browser to retry on a specific selector?
You don't need to force it—retries happen automatically in waitFor and related utilities when the underlying resolution throws a transient error. Ensure your selector is valid but potentially matches zero elements initially; the polling loop will retry until timeout or success.
Why does my selector fail permanently when multiple elements exist?
Ego‑Browser treats ambiguous locators as permanent errors because retrying cannot resolve which element you intend. Either add an nth index to disambiguate or refine the selector to match exactly one element, as implemented in the matchCountKind logic of src/element-resolver.ts.
Can I customize the transience classification for custom resolution logic?
The ElementResolutionError class accepts an explicit kind parameter in its constructor. When extending Ego‑Browser's resolution system, instantiate errors with "transient" for recoverable conditions and "permanent" for definitive failures, following the patterns in src/element-resolver.ts.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →