# Transient vs Permanent Element Resolution Failures in ego-lite

> Understand transient vs permanent element resolution failures in ego-lite. Learn to identify temporary issues and definitive errors for faster debugging.

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

---

**In ego-lite, transient failures are temporary conditions that may succeed on retry (missing elements, pending snapshots), while permanent failures indicate definitive errors that will never resolve (invalid selectors, ambiguous matches).**

Every element lookup in ego-lite can fail in one of two mutually exclusive ways. The framework classifies these failures through the `kind` property on `ElementResolutionError` in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts). Understanding this distinction is essential for writing reliable browser automation scripts and interpreting error behavior correctly.

## How ego-lite Classifies Resolution Failures

The classification system lives in the `ElementResolutionError` class at [[`package/ego-browser/src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/element-resolver.ts)](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/element-resolver.ts). Each error instance carries a **`kind`** property with exactly two possible values:

- `"transient"` — The condition causing failure may change without code modification
- `"permanent"` — The condition will persist until the underlying logic is fixed

This binary classification determines whether ego-lite's internal wait loops continue polling or abort immediately.

### The `matchCountKind` Helper

At lines 46-49 of [`element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/element-resolver.ts), the `matchCountKind` function implements the core classification logic:

```typescript
// Simplified representation of the classification helper
function matchCountKind(matches: number): "transient" | "permanent" {
  if (matches > 1) return "permanent";  // Ambiguous selector
  return "transient";                    // Zero matches or processing issue
}

```

When multiple elements match a single selector, ego-lite treats this as **permanent** — the selector is too broad and must be refined. A single match or zero matches typically yields **transient**, suggesting timing or snapshot synchronization issues.

## Transient Failures: When to Retry

**Transient failures** represent temporary state mismatches between the agent's view and the actual DOM. These conditions often self-resolve within milliseconds or after the next snapshot cycle.

### Common Transient Scenarios

- The target element has not yet rendered (page load in progress)
- A ref-ID reference exists but the corresponding snapshot hasn't been processed
- DOM mutations are pending synchronization with ego-lite's internal representation

The source code throws transient errors at [line 74](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/element-resolver.ts#L74) and [line 160](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/element-resolver.ts#L160) when ref lookups fail due to missing snapshot data. These are explicitly recoverable conditions.

### Handling Transient Errors in Practice

Ego-lite's wait utilities check `err.kind === "transient"` before continuing. In [[`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts) around line 505](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/driver/waits.ts#L505), the polling loop catches `ElementResolutionError` and only continues waiting for transient kinds:

```typescript
// Pattern from waits.ts — simplified for clarity
while (Date.now() < deadline) {
  try {
    const element = await resolve(locator);
    return element;
  } catch (err) {
    if (err instanceof ElementResolutionError && err.kind === "transient") {
      // Continue polling — element may appear
      await sleep(pollInterval);
      continue;
    }
    // Permanent error — abort immediately
    throw err;
  }
}

```

Custom retry logic follows the same pattern:

```typescript
import { ElementResolutionError } from "ego-lite";

async function robustFind(locator: string, maxAttempts = 3) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await page.findElement(locator);
    } catch (err) {
      const isTransient =
        err instanceof ElementResolutionError && err.kind === "transient";
      
      if (isTransient && attempt < maxAttempts) {
        await page.waitForTimeout(200 * attempt); // Exponential backoff
        continue;
      }
      throw err; // Permanent or final attempt
    }
  }
}

```

## Permanent Failures: When to Stop and Fix

**Permanent failures** signal fundamental problems that no amount of waiting will resolve. Continuing to retry wastes time and obscures the actual issue.

### Common Permanent Scenarios

- **Invalid selector syntax** — malformed CSS or XPath expressions
- **Ambiguous selectors** — locators matching multiple elements (handled by `matchCountKind`)
- **Structural page changes** — elements permanently moved or removed from the DOM
- **Logical errors** — wildcards or semantic (LLM-generated) locators that don't match any valid path

Permanent errors propagate immediately through ego-lite's call stack. In [[`src/driver/locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/locator.ts)](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/driver/locator.ts), fallback logic checks error kinds and only attempts alternatives for transient failures.

### Fatal Error Pattern Example

```typescript
// Permanent error construction (conceptual, from element-resolver.ts)
if (matches.length > 1) {
  throw new ElementResolutionError(
    `Locator "${selector}" matches ${matches.length} elements. ` +
    `Use a more specific selector or index.`,
    "permanent"  // Retrying will always find the same ambiguity
  );
}

```

When your code receives a permanent error, the required action is **code change**, not **wait longer**:

```typescript
try {
  await page.click("button");  // Too vague — permanent if multiple buttons exist
} catch (err) {
  if (err instanceof ElementResolutionError && err.kind === "permanent") {
    // Corrective action: refine the selector
    await page.click('[data-testid="submit-button"]');
  }
}

```

## Key Implementation Files

These source files demonstrate how the classification propagates through ego-lite's architecture:

| File | Purpose | Error Handling Pattern |
|------|---------|------------------------|
| [`package/ego-browser/src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/element-resolver.ts) | Defines `ElementResolutionError` and `matchCountKind` | Creates errors with appropriate `kind` |
| [`package/ego-browser/src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/driver/waits.ts) | Polling-based wait utilities | Continues loop only for `kind === "transient"` at line 505 |
| [`package/ego-browser/src/driver/locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/driver/locator.ts) | Locator resolution and fallbacks | Skips alternatives on permanent errors |
| [`package/ego-browser/src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts) | Public API surface | Exposes resolution results to agent code |

## Summary

- **`ElementResolutionError.kind`** at [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) is the single source of truth for failure classification in ego-lite
- **Transient failures** (`"transient"`) indicate timing or synchronization issues — retry with `waitForElement` or custom polling loops
- **Permanent failures** (`"permanent"`) indicate selector or logic errors — require code changes to resolve
- The **`matchCountKind`** helper marks ambiguous multi-match selectors as permanent at lines 46-49
- Built-in wait utilities automatically handle the distinction; custom code should check `err.kind` before deciding to retry

## Frequently Asked Questions

### How do I know if I should retry a failed element lookup?

Check the `kind` property on the caught error. If `err.kind === "transient"`, the failure may resolve on retry — use a wait loop or `waitForElement`. If `err.kind === "permanent"`, retrying will never succeed; fix your selector or investigate why the element is permanently unresolvable.

### Can a transient error become permanent?

No — the classification is deterministic based on the failure context. However, repeated transient failures within a timeout period may surface as a timeout error rather than becoming permanent. The underlying condition (missing element, pending snapshot) either resolves or the operation times out.

### What causes "permanent" errors for valid-looking selectors?

The most common cause is **selector ambiguity**: when multiple elements match, `matchCountKind` returns `"permanent"` because narrowing requires human judgment. Refine selectors with attributes, indices, or relative positioning to eliminate ambiguity and convert the failure to transient (zero matches) or success (single match).