# How ego-lite's element-resolver Classifies Failures as Retryable vs Permanent

> Learn how ego-lite's element-resolver classifies failures. Understand transient vs permanent errors with ElementResolutionError kind for efficient retries.

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

---

**ego-lite wraps every element resolution failure in an `ElementResolutionError` with a `kind` field set to `"transient"` for retryable errors or `"permanent"` for unrecoverable ones.**

The [`element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/element-resolver.ts) file in ego-lite is the single source of truth for this classification. When a CDP-based element lookup fails, the resolver does not simply throw a generic error. Instead, it inspects the failure context and explicitly chooses whether the problem might resolve itself (transient) or requires code changes (permanent).

## The ElementResolutionError Class

At the heart of this system is the error class defined in [`package/ego-browser/src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/element-resolver.ts):

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

```

Every failure path in the resolver instantiates this class with the appropriate `kind`. Callers in [`driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/driver/waits.ts) and [`driver/locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/driver/locator.ts) then branch on `err.kind` to decide whether to retry or abort.

## How element-resolver Decides: Transient vs Permanent

The classification logic follows consistent patterns based on the nature of the failure.

### Selector Match Count Failures

The `matchCountKind()` function (lines 46-49) parses error messages about match counts:

- **More than one match** → `permanent` (ambiguous selector cannot self-resolve)
- **Zero matches** → `transient` (element may appear in a future DOM snapshot)

This applies to both CSS/XPath selectors and generic locators. For example, `locatorCount()` at line 38 returns zero → transient; line 49 finds more than one → permanent.

### Invalid Selector Syntax

When CDP evaluation throws an exception, `selectorResolutionError()` (line 52) always returns `permanent`. A syntax error in the selector string requires a code change to fix.

### Reference Resolution Failures

In `resolveElementCenter()` and `resolveElementObjectId()`, missing or stale references (line 74) are `transient`. The backend node ID may exist in a subsequent snapshot, so retrying is worthwhile.

### Accessibility Tree Failures

Two distinct cases in `findBackendNodeIdsByRoleName()` and `findUniqueBackendNodeIdByRoleName()`:

| Condition | Classification | Reason |
|-----------|---------------|--------|
| `backendDOMNodeId` missing from AX node (line 58) | `permanent` | Configuration problem; no DOM node to address |
| Zero role matches (line 76) | `transient` | Element not yet rendered |
| Multiple role matches (line 82) | `permanent` | Ambiguous role/name criteria |

## Practical Error Handling Examples

### Handling Retryable Resolution Errors

```typescript
import { resolveElementCenter, ElementResolutionError } from "./element-resolver.js";

try {
  const { x, y } = await resolveElementCenter(
    cdp,
    sessionId,
    refMap,
    "loc=css:button.submit"
  );
} catch (e) {
  if (e instanceof ElementResolutionError) {
    if (e.kind === "transient") {
      // Retry after delay — DOM may update
      await delay(100);
      return retry();
    } else {
      // Permanent — selector needs fixing
      throw new Error(`Invalid selector: ${e.message}`);
    }
  }
}

```

### Detecting Ambiguous Role-Based Locators

```typescript
import { resolveElementObjectId, ElementResolutionError } from "./element-resolver.js";

try {
  const obj = await resolveElementObjectId(
    cdp,
    sessionId,
    refMap,
    "role=button name=Submit"
  );
} catch (e) {
  if (e instanceof ElementResolutionError && e.kind === "permanent") {
    // Refine role criteria or add name disambiguation
    console.error("Ambiguous locator — multiple buttons match 'Submit'");
  }
}

```

## How Callers Consume the Classification

The `kind` field drives retry logic throughout ego-lite's driver layer. In [`package/ego-browser/src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/driver/waits.ts), wait loops inspect `err.kind` to determine whether to continue polling. The [`locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/locator.ts) wrapper propagates the same classification to higher-level APIs. This creates a clean separation: the resolver decides *what* went wrong; the caller decides *what to do* about it.

## Summary

- **Transient (`"transient"`)**: Zero matches, missing refs, stale snapshots — conditions that may resolve with time or DOM updates
- **Permanent (`"permanent"`)**: Multiple matches, invalid syntax, missing `backendDOMNodeId` — conditions requiring code or configuration changes
- **Source of truth**: [`package/ego-browser/src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/element-resolver.ts) makes all `kind` decisions at error creation time
- **Consumption pattern**: Callers in [`waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/waits.ts) and [`locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/locator.ts) branch on `err.kind` for retry vs abort logic

## Frequently Asked Questions

### What makes an element resolution error retryable in ego-lite?

A failure is **transient** when the underlying condition might self-resolve: zero matches (element not yet in DOM), missing or stale reference IDs, or temporary snapshot inconsistencies. The resolver assumes the DOM may update and the same operation could succeed later.

### Why are multiple matches classified as permanent failures?

Multiple matches indicate an **ambiguous selector** that will not become unambiguous without code changes. Whether caused by overly broad CSS selectors or non-unique role/name combinations, the resolver treats this as a permanent `ElementResolutionError` because retrying cannot reduce the match count.

### Where should I handle ElementResolutionError in my code?

Handle it at the point of element interaction or in wrapper utilities. The [`driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/driver/waits.ts) and [`driver/locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/driver/locator.ts) files in ego-lite demonstrate the recommended pattern: catch `ElementResolutionError`, inspect `err.kind`, retry if transient, and propagate or abort if permanent.

### Can I override the retryable vs permanent classification?

No — the classification is hardcoded in [`element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/element-resolver.ts) based on failure type. If you need different retry behavior, wrap the resolver call and implement your own retry policy based on `err.kind`, or modify the selector to avoid permanent error conditions like ambiguity or syntax errors.