# How Ego-Lite Classifies Element-Resolution Errors as Transient or Permanent

> Discover how Ego-Lite classifies element-resolution errors as transient or permanent using the kind field. Learn how this distinction impacts retries and aborts for efficient error handling.

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

---

**Ego-Lite uses a `kind` field on the `ElementResolutionError` class to distinguish transient from permanent failures, with transient errors triggering retries and permanent errors causing immediate abort.**

The **error classification system** in ego-lite determines whether an element-resolution failure might resolve on retry or is fundamentally unrecoverable. This decision logic lives primarily in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) and drives the library's automatic retry behavior.

## The ElementResolutionError Foundation

Every resolution failure flows through the `ElementResolutionError` class. The constructor at lines 4–10 accepts a `kind` parameter that must be either `"transient"` or `"permanent"`:

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

```

Callers in [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts) and [`src/driver/locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/locator.ts) inspect `err.kind` to decide whether to retry or abort.

## How Transient Failures Are Identified

Transient errors represent **temporary conditions** that may change on subsequent attempts. The resolver marks these as `"transient"` in several scenarios.

### Unknown or Missing Refs

When `refMap.get(refId)` returns `undefined`, the resolver throws a transient error (lines 73–75):

```typescript
if (!entry) {
  throw new ElementResolutionError(`Unknown ref: ${refId}`, "transient");
}

```

The ref may become available after the page re-snapshots.

### Rendering-Related Conditions

**Box-model problems** trigger transient errors when a node's box model is missing or has zero size (lines 55–62). The element likely exists in the DOM but hasn't rendered yet.

**Role/name lookup failures** via `findBackendNodeIdByRoleName` (lines 18–22) are also transient—the UI may still be updating.

### Zero Matches for Single-Element Locators

When a locator resolves to zero elements, the error is transient (lines 36–42):

```typescript
if (matches.length === 0) {
  throw new ElementResolutionError(
    `No element found for locator: ${locator}`,
    "transient"
  );
}

```

## How Permanent Failures Are Identified

Permanent errors denote **unrecoverable problems** that will never succeed regardless of retries.

### Invalid Selectors and Malformed Queries

Errors from malformed selector syntax are marked permanent (lines 85–89):

```typescript
throw new ElementResolutionError(
  `Invalid selector: ${selector}: ${message}`,
  "permanent"
);

```

### Ambiguous Locators

When a locator matches **more than one element**, the error is permanent because the locator is ambiguous. The `matchCountKind` helper (lines 46–50) implements this logic:

```typescript
function matchCountKind(message: string): "transient" | "permanent" {
  const m = /matched (\d+)/.exec(message);
  const n = m ? Number(m[1]) : 0;
  return n > 1 ? "permanent" : "transient";
}

```

A match count of **1** yields transient (might stabilize), while **>1** yields permanent (fundamentally ambiguous).

### Missing Essential Backend IDs

When an accessibility node lacks a `backendDOMNodeId` (lines 58–62), the error is permanent: the element cannot be addressed via Chrome DevTools Protocol.

## Selector Resolution Error Pattern

The `selectorResolutionError` function (lines 52–60) demonstrates the classification pipeline:

1. Inspect the CDP evaluation message
2. Extract match count using regex
3. Delegate to `matchCountKind` for the final decision
4. Fall back to `permanent` for invalid selectors

```typescript
function selectorResolutionError(message: string): ElementResolutionError {
  if (message.includes("matched")) {
    const kind = matchCountKind(message);
    return new ElementResolutionError(message, kind);
  }
  return new ElementResolutionError(message, "permanent");
}

```

## Error Consumption in the Driver

The classification only matters because downstream code acts on it. In [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts), retry loops check `err.kind === "transient"` before continuing. Permanent errors break immediately. This pattern prevents wasted retries on impossible selectors while allowing tolerance for timing-sensitive DOM operations.

## Summary

- **Transient errors** (ref unavailable, zero matches, box-model missing, single match ambiguity) indicate conditions that may resolve on retry
- **Permanent errors** (invalid selectors, multiple matches, missing backend IDs) represent fundamental failures that never succeed
- The `ElementResolutionError` class in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) carries the `kind` discriminator
- Resolution logic inspects match counts, ref existence, and DOM state to assign the correct classification
- Callers in [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts) and [`src/driver/locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/locator.ts) consume `kind` to drive retry behavior

## Frequently Asked Questions

### What makes an element-resolution error transient in Ego-Lite?

A transient error occurs when the failure might resolve on a subsequent attempt. Common causes include: the element hasn't rendered yet (missing box model), the ref isn't in the current snapshot, or a locator matches zero elements. The system assumes these conditions are timing-dependent and may stabilize.

### When does Ego-Lite classify an error as permanent?

Permanent errors indicate unrecoverable problems: invalid selector syntax, a locator matching multiple elements (ambiguity), or an accessibility node without a `backendDOMNodeId`. These failures cannot succeed regardless of how many times the operation retries.

### How does the match count determine error classification?

The `matchCountKind` function in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) parses CDP messages for "matched N elements". **N = 1** yields transient (the match might stabilize to unique), while **N > 1** yields permanent (the locator is fundamentally ambiguous). This prevents endless retries on selectors that will always match multiple elements.

### Where is error classification consumed in the codebase?

The `kind` property is read in [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts) to control retry loops and in [`src/driver/locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/locator.ts) to propagate retryable versus fatal failures. This separation allows the resolver to focus on classification while driver code handles policy.