# How the Ego-Lite Element-Resolver Classifies Errors as Transient or Permanent

> Discover how the ego-lite element-resolver classifies errors as transient or permanent. Learn about retryable issues and unresolvable ambiguities using matchCountKind.

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

---

**The `element-resolver` in `citrolabs/ego-lite` classifies resolution failures by throwing an `ElementResolutionError` with a `kind` field of `"transient"` for retryable issues and `"permanent"` for unresolvable ambiguities, using the `matchCountKind` helper in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) to assign the category based on how many elements a locator matches.**

When a browser automation locator fails in the `citrolabs/ego-lite` framework, the `element-resolver` does not simply crash—it categorizes the failure so the caller knows whether to retry or abort. Located in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts), the resolver distinguishes between **transient** errors that may resolve after a fresh snapshot and **permanent** errors caused by inherently ambiguous selectors. This article explains the exact logic, source code paths, and helper functions that drive that classification.

## The `ElementResolutionError` Kind System

The core mechanism is a custom error class that carries a discriminator property. Instead of a generic exception, every resolution failure raises an **`ElementResolutionError`** annotated with a **`kind`** value.

### Error Class Definition

In [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts), the `ElementResolutionError` class stores a `kind` field that is typed as `"transient"` or `"permanent"`. This field is populated at the throw site and later inspected by the driver to decide whether a re-snapshot is worthwhile.

### The `matchCountKind` Helper

The classification logic is centralized in the **`matchCountKind`** helper around lines 46-49. According to the `citrolabs/ego-lite` source code, this function accepts a numeric match count and returns the corresponding error kind:

```javascript
function matchCountKind(n) {
  return n > 1 ? 'permanent' : 'transient';
}

```

When a locator matches **more than one** element, the helper returns `"permanent"` because retrying the same selector cannot disambiguate the result without changing the locator itself. For all other counts—including zero—it returns `"transient"`, since the element may appear or become unique after the page state updates.

## Transient vs. Permanent Classification Rules

The resolver applies these kinds at distinct throw sites throughout [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts). The distinction determines whether the browser runtime should re-snapshot or immediately propagate the failure.

### Transient Error Scenarios

Transient errors represent temporary conditions that might succeed after a page refresh or retry. According to the source analysis, the resolver emits `"transient"` for cases such as:

- Unknown reference IDs.
- Zero-match selectors.
- Degenerated box-model calculations.
- Any scenario where a single element cannot yet be identified unambiguously but the DOM might change.

These explicit `"transient"` throws appear at multiple points in the file, including around lines 74, 143, and 298.

### Permanent Error Scenarios

Permanent errors signal structural problems with the locator itself. The primary trigger is an **ambiguous selector** that matches two or more elements. When the candidate collection contains multiple nodes, the resolver invokes `matchCountKind` and throws an `ElementResolutionError` with `kind` set to `"permanent"`. Because the underlying DOM structure satisfies the locator too broadly, waiting or retrying will not fix the issue—the locator itself must be narrowed.

## Runtime Resolution Flow

During execution, the resolver collects candidate elements, evaluates the result count, and either invokes `matchCountKind` or throws a known transient error directly. As implemented in `citrolabs/ego-lite`, the surrounding driver logic in [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) treats a transient `ElementResolutionError` as a signal to re-snapshot the page and attempt resolution again. Permanent errors bypass the retry loop entirely and propagate to the caller immediately, preventing wasted polling cycles on unresolvable ambiguities.

## Code Examples

The following patterns show how to consume the `kind` field in application code.

### Retry on Transient Errors

```javascript
try {
  const el = await resolveElement('loc=css:#submit-button');
  await el.click();
} catch (e) {
  if (e instanceof ElementResolutionError && e.kind === 'transient') {
    // Re-snapshot the page and try again
    await ego.refresh();
    // ... retry the resolution ...
  } else {
    throw e; // permanent or unrelated error
  }
}

```

### Abort on Permanent Errors

```javascript
try {
  const el = await resolveElement('loc=css:.ambiguous-item');
} catch (e) {
  if (e.kind === 'permanent') {
    console.error('Locator matches multiple elements - adjust the selector');
    // Abort or fallback to a different strategy
  }
}

```

### Helper That Distinguishes Both Kinds

```javascript
async function clickIfUnique(locator) {
  try {
    const el = await resolveElement(locator);
    await el.click();
    return true;
  } catch (e) {
    if (e.kind === 'permanent') {
      console.warn(`Cannot click: ${locator} is ambiguous`);
    }
    return false;
  }
}

```

## Summary

- `ElementResolutionError` in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) carries a `kind` field that is either `"transient"` or `"permanent"`.
- The `matchCountKind` helper classifies multi-element matches as `"permanent"` and everything else as `"transient"`.
- **Transient** errors cover zero-match selectors, unknown references, and degenerated box-model states that may resolve after a retry.
- **Permanent** errors indicate locator ambiguity that requires changing the selector rather than re-snapshotting.
- The ego-lite runtime uses this classification to decide whether to re-snapshot and retry or to propagate the error immediately.

## Frequently Asked Questions

### What is the difference between transient and permanent errors in ego-lite?

Transient errors are temporary resolution failures—such as missing elements or unknown reference IDs—that may disappear after the page is re-snapshotted or retried. Permanent errors are structural ambiguities, typically caused by a locator matching multiple elements, that cannot be resolved by waiting and require a corrected selector.

### How does `matchCountKind` decide the error kind?

As defined in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts), `matchCountKind` accepts a numeric match count and returns `"permanent"` when the count is greater than one. For zero or one matches, it returns `"transient"` because the condition might change on a subsequent attempt.

### Can a transient error become permanent after retries?

The classification is computed independently for each resolution attempt. A transient error such as a zero-match does not automatically convert to a permanent error after retries. If the DOM later changes so that the same locator matches multiple elements, a new resolution attempt would classify that subsequent failure as permanent.

### Where does the retry logic for transient errors live?

The retry logic resides in the browser runtime layer, specifically [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts). It catches `ElementResolutionError` instances, checks for `kind === 'transient'`, and triggers a re-snapshot and retry loop. Permanent errors are intentionally allowed to propagate without retrying.