# Handling Transient vs Permanent Element Resolution Errors in Ego-Lite

> Learn how ego-lite handles transient vs permanent element resolution errors. Understand the difference between temporary DOM issues and fatal selector problems for effective debugging.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: how-to-guide
- Published: 2026-08-27

---

**Ego-Lite throws `ElementResolutionError` instances with a `kind` property—set to either `"transient"` or `"permanent"`—to differentiate between temporary DOM conditions that may resolve on retry and fatal selector issues that require immediate failure.**

The `citrolabs/ego-lite` browser automation framework implements a deterministic error classification system in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) to handle unreliable element lookups. When resolving CSS selectors, role locators, or `@ref` identifiers fails, the runtime categorizes the failure based on whether the condition is recoverable, enabling intelligent retry logic in higher-level APIs like `waitForSelector` and `readElement`.

## The ElementResolutionError Class

At the core 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). According to the citrolabs/ego-lite source code, the error constructor accepts a message string and a `kind` discriminator:

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

```

This structure allows calling code to inspect `error.kind` and decide whether to retry the operation or abort immediately.

## Classification Logic for Error Kinds

The resolver determines the error `kind` based on the specific failure mode encountered during element lookup. As implemented in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts), the following rules apply:

**Multiple element matches.** When a selector matches more than one element but a single unique element is required, the error is **permanent**. The `matchCountKind()` helper (lines 46-50) classifies ambiguous matches as permanent because the caller must disambiguate the selector.

**Invalid selector syntax.** Syntax errors in CSS selectors or role locators result in **permanent** errors. The `selectorResolutionError()` function (lines 52-60) marks these as permanent since an invalid selector cannot become valid on subsequent retries.

**Unknown `@ref` identifiers.** References to snapshot IDs that do not exist in the current DOM generate **transient** errors (lines 74-75). The element may appear after the next snapshot, making retry appropriate.

**Missing box model or stale backend nodes.** When the Chrome DevTools Protocol reports a missing box model or stale node, [`element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/element-resolver.ts) (lines 58-62) treats these as **transient** conditions indicating the element is not yet rendered or ready.

**Accessibility nodes without backend DOM node IDs.** If an AX node lacks a `backendDOMNodeId`, the error is **permanent** (lines 59-63) because the accessibility node cannot be linked to a concrete DOM element.

**Zero locator matches.** When a locator finds no elements, the error is **transient** because the element may appear later after navigation or DOM updates.

## How Callers Handle Each Error Type

Higher-level APIs 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) implement distinct strategies based on the error classification.

### waitForSelector Retry Logic

The `waitForSelector` function in [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts) (lines 505-509) catches `ElementResolutionError` and implements polling logic:

```typescript
try {
  handle = await resolveHandle(selector);
} catch (err) {
  if (err instanceof ElementResolutionError && err.kind === "transient") {
    await state.sleep(300);
    continue;               // keep polling
  }
  throw err;                // permanent → fail fast
}

```

**Transient** errors trigger a 300ms sleep and retry loop until timeout, while **permanent** errors propagate immediately to prevent wasted cycles on impossible selectors.

### readElement Polling Behavior

The `readElement` utility in [`src/driver/locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/locator.ts) (lines 11-18) implements deadline-based polling for transient failures:

```typescript
if (!(error instanceof ElementResolutionError) ||
    error.kind !== "transient" ||
    state.now() >= deadline) {
  throw error;   // permanent or timeout
}
await state.sleep(Math.min(100, deadline - state.now()));

```

This pattern retries transient resolution errors with short sleeps until the default timeout expires, ensuring resilience against temporary DOM churn.

### readOptionalElement Fallback Strategy

For optional elements, `readOptionalElement` in [`src/driver/locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/locator.ts) (lines 36-40) treats transient errors as "element not present" rather than exceptions:

```typescript
} catch (error) {
  if (error instanceof ElementResolutionError && error.kind === "transient") {
    return fallback;
  }
  throw error;
}

```

**Permanent** errors still propagate to signal configuration bugs, while **transient** errors gracefully return the fallback value.

## Practical Implementation Examples

### Example 1: Using waitForSelector for Robust Waiting

The high-level `waitForSelector` helper automatically handles the transient/permanent distinction:

```typescript
// Wait up to the default timeout for a button to become visible.
await waitForSelector('button[data-testid="submit"]', { state: "visible" });

```

If the button is not yet rendered, the underlying `ElementResolutionError` is transient, triggering retries. If the selector is malformed, a permanent error rejects the promise immediately.

### Example 2: Manual Retry with resolveElementCenter

For custom interactions, manually check `error.kind` before retrying:

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

async function clickCenter(cdp, sessionId, refMap, selectorOrRef) {
  try {
    const { x, y, sessionId: sid } = await resolveElementCenter(
      cdp,
      sessionId,
      refMap,
      selectorOrRef,
    );
    await cdp("Input.dispatchMouseEvent", { type: "mousePressed", x, y }, sid);
  } catch (e) {
    if (e instanceof ElementResolutionError && e.kind === "transient") {
      // Retry after a short pause – element may appear later.
      await new Promise(r => setTimeout(r, 200));
      return clickCenter(cdp, sessionId, refMap, selectorOrRef);
    }
    // Permanent error: surface to the user.
    throw e;
  }
}

```

### Example 3: Optional Element Fetching

Use `readOptionalElement` to safely attempt retrieval with a fallback:

```typescript
import { readOptionalElement } from "./driver/locator.js";

const maybeButton = await readOptionalElement(
  'button[data-testid="optional"]',
  'function(el){ return el?.innerText; }',
  [],
  null, // fallback when not present
);
if (maybeButton !== null) {
  console.log("Button text:", maybeButton);
}

```

If the selector yields a transient error, the function returns `null` without throwing.

## Summary

- **Transient errors** indicate temporary DOM conditions—such as elements not yet rendered, stale backend nodes, unknown `@ref` IDs, or zero matches—that may resolve on retry. The runtime implements automatic polling and waiting strategies when encountering these.
- **Permanent errors** signal fundamental issues—ambiguous selectors matching multiple elements, invalid syntax, or orphaned accessibility nodes—that cannot resolve through waiting. These propagate immediately to alert developers of configuration errors.
- The classification occurs in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) through the `ElementResolutionError` class and helper functions like `matchCountKind()` and `selectorResolutionError()`.
- Higher-level APIs 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 this classification to implement fail-fast behavior for permanent issues and resilient polling for transient ones.

## Frequently Asked Questions

### How does Ego-Lite decide if an element resolution error is transient or permanent?

Ego-Lite examines the specific failure mode in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts). Invalid selector syntax and ambiguous matches (multiple elements) produce permanent errors because the code requires correction. Missing elements, stale backend nodes, and unknown `@ref` IDs generate transient errors because the DOM may update to satisfy the lookup on subsequent attempts.

### What happens when waitForSelector encounters a permanent resolution error?

When `waitForSelector` in [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts) catches an `ElementResolutionError` with `kind === "permanent"`, it re-throws the error immediately without additional retries. This fail-fast behavior prevents infinite polling loops on malformed selectors or ambiguous locators.

### Can I handle transient resolution errors manually in my Ego-Lite scripts?

Yes. Import `ElementResolutionError` from [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) and check the `kind` property in your catch blocks. If the error is transient, implement a retry loop with exponential backoff or return a fallback value, while allowing permanent errors to propagate to the caller.

### Why does readOptionalElement return a fallback for transient errors but throw for permanent ones?

The design treats transient errors as indicating "element not yet available," which aligns with the semantic meaning of an optional element being absent. Permanent errors indicate a broken selector or DOM structure that will never resolve, so the function throws to alert you of the configuration bug rather than silently returning a fallback.