How ego-lite's element-resolver Classifies Failures as Retryable vs Permanent

ego-lite wraps every element resolution failure in an ElementResolutionError with a kind field set to "transient" for retryable errors or "permanent" for unrecoverable ones.

The element-resolver.ts file in ego-lite is the single source of truth for this classification. When a CDP-based element lookup fails, the resolver does not simply throw a generic error. Instead, it inspects the failure context and explicitly chooses whether the problem might resolve itself (transient) or requires code changes (permanent).

The ElementResolutionError Class

At the heart of this system is the error class defined in package/ego-browser/src/element-resolver.ts:

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

Every failure path in the resolver instantiates this class with the appropriate kind. Callers in driver/waits.ts and driver/locator.ts then branch on err.kind to decide whether to retry or abort.

How element-resolver Decides: Transient vs Permanent

The classification logic follows consistent patterns based on the nature of the failure.

Selector Match Count Failures

The matchCountKind() function (lines 46-49) parses error messages about match counts:

  • More than one matchpermanent (ambiguous selector cannot self-resolve)
  • Zero matchestransient (element may appear in a future DOM snapshot)

This applies to both CSS/XPath selectors and generic locators. For example, locatorCount() at line 38 returns zero → transient; line 49 finds more than one → permanent.

Invalid Selector Syntax

When CDP evaluation throws an exception, selectorResolutionError() (line 52) always returns permanent. A syntax error in the selector string requires a code change to fix.

Reference Resolution Failures

In resolveElementCenter() and resolveElementObjectId(), missing or stale references (line 74) are transient. The backend node ID may exist in a subsequent snapshot, so retrying is worthwhile.

Accessibility Tree Failures

Two distinct cases in findBackendNodeIdsByRoleName() and findUniqueBackendNodeIdByRoleName():

Condition Classification Reason
backendDOMNodeId missing from AX node (line 58) permanent Configuration problem; no DOM node to address
Zero role matches (line 76) transient Element not yet rendered
Multiple role matches (line 82) permanent Ambiguous role/name criteria

Practical Error Handling Examples

Handling Retryable Resolution Errors

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

try {
  const { x, y } = await resolveElementCenter(
    cdp,
    sessionId,
    refMap,
    "loc=css:button.submit"
  );
} catch (e) {
  if (e instanceof ElementResolutionError) {
    if (e.kind === "transient") {
      // Retry after delay — DOM may update
      await delay(100);
      return retry();
    } else {
      // Permanent — selector needs fixing
      throw new Error(`Invalid selector: ${e.message}`);
    }
  }
}

Detecting Ambiguous Role-Based Locators

import { resolveElementObjectId, ElementResolutionError } from "./element-resolver.js";

try {
  const obj = await resolveElementObjectId(
    cdp,
    sessionId,
    refMap,
    "role=button name=Submit"
  );
} catch (e) {
  if (e instanceof ElementResolutionError && e.kind === "permanent") {
    // Refine role criteria or add name disambiguation
    console.error("Ambiguous locator — multiple buttons match 'Submit'");
  }
}

How Callers Consume the Classification

The kind field drives retry logic throughout ego-lite's driver layer. In package/ego-browser/src/driver/waits.ts, wait loops inspect err.kind to determine whether to continue polling. The locator.ts wrapper propagates the same classification to higher-level APIs. This creates a clean separation: the resolver decides what went wrong; the caller decides what to do about it.

Summary

  • Transient ("transient"): Zero matches, missing refs, stale snapshots — conditions that may resolve with time or DOM updates
  • Permanent ("permanent"): Multiple matches, invalid syntax, missing backendDOMNodeId — conditions requiring code or configuration changes
  • Source of truth: package/ego-browser/src/element-resolver.ts makes all kind decisions at error creation time
  • Consumption pattern: Callers in waits.ts and locator.ts branch on err.kind for retry vs abort logic

Frequently Asked Questions

What makes an element resolution error retryable in ego-lite?

A failure is transient when the underlying condition might self-resolve: zero matches (element not yet in DOM), missing or stale reference IDs, or temporary snapshot inconsistencies. The resolver assumes the DOM may update and the same operation could succeed later.

Why are multiple matches classified as permanent failures?

Multiple matches indicate an ambiguous selector that will not become unambiguous without code changes. Whether caused by overly broad CSS selectors or non-unique role/name combinations, the resolver treats this as a permanent ElementResolutionError because retrying cannot reduce the match count.

Where should I handle ElementResolutionError in my code?

Handle it at the point of element interaction or in wrapper utilities. The driver/waits.ts and driver/locator.ts files in ego-lite demonstrate the recommended pattern: catch ElementResolutionError, inspect err.kind, retry if transient, and propagate or abort if permanent.

Can I override the retryable vs permanent classification?

No — the classification is hardcoded in element-resolver.ts based on failure type. If you need different retry behavior, wrap the resolver call and implement your own retry policy based on err.kind, or modify the selector to avoid permanent error conditions like ambiguity or syntax errors.

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 →