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

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 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. This specialized error type carries a discriminator field that downstream consumers use to implement intelligent retry policies.

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

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. At line 505, the wait loop explicitly checks the error classification before attempting another resolution:

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:

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:

// 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.
  • Transient errors (unknown refs, stale nodes, zero matches) indicate temporary states and trigger retry loops in 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 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 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 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).

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 →