# How the Element Resolver Differentiates Transient vs Permanent Errors for Retry Logic in Ego-Lite

> Learn how Ego-Lite's element resolver differentiates transient vs permanent errors for retry logic. Understand the `kind` property of ElementResolutionError for smarter retries.

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

---

**The Ego-Lite element resolver categorizes every lookup failure as either `"transient"` (retryable) or `"permanent"` (non-retryable) by inspecting where and why the failure occurred, then exposes this distinction through the `kind` property of `ElementResolutionError`.**

The **element‑resolver** module sits at the heart of the Ego Browser's element-lookup pipeline. Every public helper — `resolveElementCenter`, `resolveElementObjectId`, `resolveLocatorCenter`, and others — delegates to this resolver. When lookups fail, the resolver throws an `ElementResolutionError` with a **`kind`** field that signals whether the caller should retry or abort immediately. This design provides a single source of truth for retry semantics across the entire API.

## The ElementResolutionError Class

The error classification begins with a simple, strongly-typed structure. In [`package/ego-browser/src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/element-resolver.ts), the `ElementResolutionError` class is defined as:

```ts
class ElementResolutionError extends Error {
  kind: "transient" | "permanent";
  constructor(message: string, kind: "transient" | "permanent") {
    super(message);
    this.name = "ElementResolutionError";
    this.kind = kind;
  }
}

```

All resolver entry points catch low-level CDP or runtime errors and re-throw them as `ElementResolutionError` with the appropriate `kind`. Higher-level helpers like `waitForSelector` interpret this field to decide whether to poll again or propagate the failure.

## How the Resolver Chooses Error Kind

The resolver's decision logic inspects **failure context** — what went wrong and where in the pipeline it happened. Here are the specific rules implemented in [`element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/element-resolver.ts):

### Selector Syntax Errors

When JavaScript evaluation throws an exception containing a "matched N elements" message, the helper `matchCountKind()` (lines 46–50) parses the message:

- **More than one element matched** → **permanent** (ambiguous selector)
- **Zero or one element matched** → **transient** (element may appear later)

```ts
// From element-resolver.ts lines 46-50
function matchCountKind(message: string): "transient" | "permanent" {
  const match = /matched (\d+) elements/.exec(message);
  if (!match) return "transient";
  return parseInt(match[1], 10) > 1 ? "permanent" : "transient";
}

```

### Generic Selector Failures

The `selectorResolutionError()` function (lines 52–60) always creates a **permanent** error when the selector cannot be parsed or evaluates to an invalid expression. The selector itself is malformed — retrying will not help.

### Unknown or Stale References

References using the `@N` syntax that point to non-existent backend nodes are treated as **transient** (lines 73–76). The DOM may change or a re-snapshot may restore validity.

### Missing Box Model

When `boxModelCenter` encounters an element without computed box model data (not rendered or zero-sized), it throws a **transient** error (lines 55–62). Callers poll until the element becomes visible.

### Role-Based Lookup Results

Two functions handle role-based queries differently:

- **`findBackendNodeIdByRoleName`** (lines 18–22): Throws **transient** when no match is found — the element may appear.
- **`findUniqueBackendNodeIdByRoleName`** (lines 70–88): Throws **permanent** when more than one match is returned — the role qualifier is ambiguous.

### Locator Resolution Results

The `resolveLocatorObjectId` function applies count-based logic:

- **Zero elements matched** (lines 36–43): **Transient** — element may appear later.
- **More than one element when single required** (lines 48–55): **Permanent** — locator is ambiguous.

### AX Tree Mapping Failures

When `findBackendNodeIdsByRoleName` encounters an accessibility node lacking `backendDOMNodeId` (lines 58–63), it throws **permanent**. The underlying page structure cannot be resolved through the accessibility tree.

## Implementing Retry Logic with Error Kinds

The `kind` property enables clean, consistent retry implementation. Here's a pattern used throughout the codebase:

```ts
while (true) {
  try {
    await resolveElementCenter(cdp, sessionId, refMap, ref);
    break; // success
  } catch (e) {
    if (e instanceof ElementResolutionError && e.kind === "transient") {
      await sleep(100); // poll again
      continue;
    }
    throw e; // permanent → propagate immediately
  }
}

```

### Manual Resolver Usage with Explicit Retry

For custom automation scripts, you can implement your own retry loop:

```ts
import { resolveElementCenter, ElementResolutionError } from "ego-browser/src/element-resolver.js";

async function clickRef(ref, cdp, sessionId, refMap) {
  const maxRetries = 50;
  let attempts = 0;
  
  while (attempts++ < maxRetries) {
    try {
      const { x, y, sessionId: sid } = await resolveElementCenter(
        cdp,
        sessionId,
        refMap,
        ref
      );
      await cdp.sendRaw("Input.dispatchMouseEvent", {
        type: "mousePressed",
        x,
        y,
        button: "left"
      }, sid);
      return; // success
    } catch (e) {
      if (e instanceof ElementResolutionError && e.kind === "transient") {
        await new Promise(r => setTimeout(r, 150));
        continue; // retry after short pause
      }
      throw e; // permanent → give up immediately
    }
  }
  throw new Error(`Failed to click ${ref} after ${maxRetries} attempts`);
}

```

### Using Higher-Level Helpers

Most users rely on helpers that embed this logic automatically:

```ts
import { click } from "ego-browser/src/helpers.js";

// Internally handles transient retries with exponential backoff
await click("@5");

```

The `click`, `waitForSelector`, and similar helpers in [`helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/helpers.ts) consume the resolver's errors and implement appropriate retry strategies based on `kind`.

## Key Source Files

| File | Purpose | Critical Lines |
|------|---------|--------------|
| [`package/ego-browser/src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/element-resolver.ts) | Error classification, `ElementResolutionError` class, all resolver entry points | 18–88 (core logic), 46–50 (`matchCountKind`), 52–60 (`selectorResolutionError`), 70–88 (role-based resolution) |
| [`package/ego-browser/src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts) | Public API (`click`, `waitForSelector`) consuming resolver errors | Retry loop implementations |
| `package/ego-browser/src/element-resolver.test.mjs` | Test coverage for transient vs permanent scenarios | Edge case validation |

## Summary

- **Transient errors** (`kind: "transient"`) indicate conditions that may resolve with time: missing elements, stale references, unrendered nodes, and zero-count locator results. Callers should retry.

- **Permanent errors** (`kind: "permanent"`) indicate fundamental problems: ambiguous selectors, malformed expressions, multiple matches when one required, and unresolvable AX tree mappings. Callers should abort.

- The `ElementResolutionError` class in [`element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/element-resolver.ts) provides the single source of truth for this classification.

- All resolver functions (`resolveElementCenter`, `resolveElementObjectId`, `resolveLocatorObjectId`, etc.) normalize failures into this error type with appropriate `kind` values.

- Higher-level APIs leverage this categorization for automatic retry behavior without exposing complexity to end users.

## Frequently Asked Questions

### What happens when a selector matches multiple elements?

The resolver throws a **permanent** `ElementResolutionError`. In `resolveLocatorObjectId` (lines 48–55), when a single element is required but multiple are found, the error `kind` is set to `"permanent"` because the selector is ambiguous. Retry would repeatedly match the same multiple elements.

### Can I override the transient/permanent classification?

No. The `kind` property is determined entirely by the resolver's internal logic based on failure context. However, you can intercept any `ElementResolutionError` and implement custom fallback behavior before deciding whether to retry or abort.

### How does the resolver handle stale element references?

References prefixed with `@` that no longer map to valid backend node IDs produce **transient** errors (lines 73–76). The DOM may have changed through JavaScript execution, and a subsequent re-snapshot may restore reference validity. This enables robust automation against dynamic single-page applications.

### What's the performance impact of transient error retries?

Transient retries with short sleeps (typically 50–150ms) introduce minimal overhead for element appearance scenarios. For permanent errors, the resolver fails fast without polling, avoiding wasted cycles on unrecoverable conditions. The test suite in `element-resolver.test.mjs` validates that permanent errors propagate immediately.