Transient vs Permanent Element Resolution Failures in ego-lite

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. 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). 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, the matchCountKind function implements the core classification logic:

// 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 and line 160 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 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:

// 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:

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/package/ego-browser/src/driver/locator.ts), fallback logic checks error kinds and only attempts alternatives for transient failures.

Fatal Error Pattern Example

// 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:

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 Defines ElementResolutionError and matchCountKind Creates errors with appropriate kind
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 Locator resolution and fallbacks Skips alternatives on permanent errors
package/ego-browser/src/helpers.ts Public API surface Exposes resolution results to agent code

Summary

  • ElementResolutionError.kind at 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).

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →