# Ego‑Browser Element Resolution Transience Rules: When to Retry vs. Fail

> Understand ego-browser element resolution transience rules. Learn when to retry failures like unknown references and when to fail on permanent issues such as ambiguous selectors.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: deep-dive
- Published: 2026-08-29

---

**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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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.getBoxModel` returns 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.evaluate` or a selector query returns `null`, 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 `findBackendNodeIdsByRoleName` returns 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 `nth` parameter is provided. The error is thrown with `kind: "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 `ElementResolutionError` with `"permanent"` directly, such as syntactically invalid CSS selectors.

The `matchCountKind` helper in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts). The generic wait loop uses the error kind to control flow:

```ts
// 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

```ts
// 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)

```ts
// 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

```ts
// 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`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) | Defines `ElementResolutionError`, implements `matchCountKind`, and encodes transient/permanent decisions for all resolution scenarios |
| [`src/driver/locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/locator.ts) | Wraps element lookups and propagates error kinds to calling code |
| [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/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.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) via `ElementResolutionError`, propagates through [`src/driver/locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/locator.ts), and controls flow in [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts).

- Design selectors to be unambiguous; use explicit `nth` parameters 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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts).