# How Ego-Browser Differentiates Between Transient and Permanent Errors During Element Resolution

> Learn how ego-browser distinguishes transient from permanent errors in element resolution. Discover the retry logic to optimize your test stability.

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

---

**Ego-browser classifies element resolution failures using a custom `ElementResolutionError` class with a `kind` property set to either `"transient"` or `"permanent"`, enabling the wait logic in [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts) to retry only recoverable transient errors while failing fast on permanent configuration issues.**

The `citrolabs/ego-lite` repository implements a robust error-handling strategy for browser automation that distinguishes between temporary DOM state issues and unrecoverable selector problems. During element resolution through Chrome DevTools Protocol (CDP), the framework analyzes failure contexts to assign the appropriate error classification, ensuring efficient automation workflows that avoid infinite retry loops on permanent failures.

## The ElementResolutionError Classification System

At the foundation of this mechanism lies the `ElementResolutionError` class defined in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts). This specialized error type carries a discriminator field that downstream consumers use to implement intelligent retry policies.

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

```

The `kind` property is immutable after construction and serves as the single source of truth for determining whether a resolution failure warrants another attempt or immediate propagation.

## Transient Errors: Retryable Conditions

**Transient errors** represent temporary states where the target element might become available if the automation waits for the DOM to stabilize. The framework assigns `"transient"` kind in several specific scenarios:

- **Unknown ref ID**: When `resolveElementCenter` or `resolveElementObjectId` (lines 74-75) cannot locate the reference in the ref map
- **Stale backend nodes**: When CDP calls fail after DOM mutations invalidate previously valid node references
- **Missing box model**: When `DOM.getBoxModel` returns insufficient data in `boxModelCenter` (lines 58-62)
- **Zero role matches**: When `findBackendNodeIdByRoleName` finds no elements (lines 18-21)
- **Zero locator count**: When `resolveLocatorObjectId` and `resolveLocatorCenter` find no matching elements (lines 37-41)

```typescript
// Example: Transient error for unknown reference
if (!entry) {
  throw new ElementResolutionError(`Unknown ref: ${refId}`, "transient");
}

```

## Permanent Errors: Non-Retryable Failures

**Permanent errors** indicate selector syntax issues or ambiguous queries that will not resolve through retry. These receive the `"permanent"` kind to trigger immediate failure:

- **Invalid selector syntax**: When CDP `Runtime.evaluate` throws an exception during selector parsing (lines 53-60)
- **Multiple unexpected matches**: When the `matchCountKind` helper (lines 46-50) detects "matched N elements" where N > 1 for single-target selectors
- **Ambiguous role lookups**: When `findUniqueBackendNodeIdByRoleName` finds multiple matching elements (lines 82-86)
- **Unclear locator resolution**: When `resolveLocatorObjectId` encounters multiple elements without an explicit index (lines 49-53)

```typescript
// Example: Permanent error for invalid selector syntax
return new ElementResolutionError(
  `Invalid selector: ${selector}: ${message}`,
  "permanent",
);

```

## Match Count Analysis and Error Classification

The framework dynamically determines error kinds by inspecting failure messages. The `matchCountKind` function (lines 46-50) uses regex parsing to classify selector failures based on match counts:

```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";
}

```

This logic ensures that matching zero elements (potentially a timing issue) is treated as transient, while matching multiple elements when only one was expected is a permanent configuration error.

## Retry Logic Implementation in Wait Helpers

The `kind` field drives retry behavior in [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts). At line 505, the wait loop explicitly checks the error classification before attempting another resolution:

```typescript
if (err instanceof ElementResolutionError && err.kind === "transient") {
  // retry the wait
}

```

This conditional prevents wasted cycles on permanent failures while allowing the DOM to stabilize for transient issues such as race conditions or animation delays.

## Practical Error Handling Examples

When consuming the resolution API directly, you can leverage the classification to implement appropriate recovery strategies:

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

try {
  const { x, y } = await resolveElementCenter(cdp, sessionId, refMap, "button.save");
  console.log(`Center at (${x}, ${y})`);
} catch (e) {
  if (e instanceof ElementResolutionError) {
    if (e.kind === "transient") {
      // The element might appear later – retry after a short delay
      console.log("Transient error, will retry:", e.message);
    } else {
      // Permanent failure – abort or raise a user-visible error
      console.error("Permanent error, cannot proceed:", e.message);
    }
  } else {
    // Unexpected error
    console.error(e);
  }
}

```

For automated retry handling, use the built-in wait helpers that respect the error classification:

```typescript
// Automatically retries transient failures, fails immediately on permanent errors
await waitFor(async () => {
  await resolveElementCenter(cdp, sessionId, refMap, "role:button[name=Submit]");
});

```

## Summary

- Ego-browser assigns all element resolution failures a **classification** of either `"transient"` or `"permanent"` through the `ElementResolutionError` class in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts).
- **Transient errors** (unknown refs, stale nodes, zero matches) indicate temporary states and trigger retry loops in [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts).
- **Permanent errors** (invalid syntax, ambiguous matches) represent configuration failures that cause immediate termination to prevent infinite loops.
- The classification logic inspects **match counts**, CDP response states, and selector validity to determine the appropriate `kind` property.
- Higher-level wait helpers consume this metadata at line 505 of [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts) to implement efficient, context-aware retry policies.

## Frequently Asked Questions

### What conditions trigger a transient error in ego-browser?

Transient errors occur when elements are temporarily unavailable due to timing or DOM state issues. Specific triggers include unknown ref IDs in the reference map (lines 74-75), stale backend nodes after DOM changes, missing box model data, and role-based lookups that return zero matches (lines 18-21). These conditions may resolve if the automation waits for the page to finish loading or animating.

### How does ego-browser handle permanent selector syntax errors?

When `Runtime.evaluate` throws an exception due to invalid selector syntax, the `selectorResolutionError` function (lines 53-60) creates an `ElementResolutionError` with `kind: "permanent"`. The wait logic in [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts) recognizes this classification and fails immediately rather than retrying, as syntax errors require code changes rather than additional wait time.

### Can custom automation scripts override the default retry behavior?

While the built-in wait helpers in [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts) automatically retry transient errors while respecting permanent failures, you can implement custom retry logic by catching `ElementResolutionError` and inspecting the `kind` property. This allows you to define alternative recovery strategies for specific transient conditions or log permanent failures for debugging before terminating execution.

### What distinguishes role-based from locator-based resolution error handling?

Role-based resolution through `findBackendNodeIdByRoleName` throws **transient** errors for zero matches but **permanent** errors when multiple elements match a unique role query (lines 82-86). Locator-based resolution follows similar semantics: zero matches are treated as transient (lines 37-41), while multiple matches without an explicit index parameter result in permanent errors (lines 49-53).