# ego-lite Element Resolution System: How Transient vs Permanent Failures Work

> Understand ego-lite's element resolution system. Learn how the kind property on ElementResolutionError differentiates transient vs permanent failures, guiding runtime decisions on polling or aborting.

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

---

**ego-lite uses a `kind` property on `ElementResolutionError` to distinguish retryable ("transient") failures from non-retryable ("permanent") failures, letting the runtime decide whether to poll or abort.**

The **ego-lite element resolution system** is the core mechanism that locates DOM elements through refs, selectors, and role-based locators. When resolution fails, the system categorizes the failure into one of two buckets—each with drastically different runtime behavior. This article breaks down how citrolabs/ego-lite implements this distinction and why it matters for building reliable browser automation.

## How ElementResolutionError Categorizes Failures

All element-resolution failures in ego-lite flow through `ElementResolutionError`, defined in [`package/ego-browser/src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/element-resolver.ts). The error carries a `kind` property that is either `"transient"` or `"permanent"`.

### Transient Failures: Retryable by Design

**Transient failures** indicate the lookup *might succeed later*. The runtime responds by retrying until the element appears or a timeout expires.

Common transient scenarios include:

- **Missing box model** — `resolveElementCenter` throws transient when `boxModel` is absent or malformed (content array has fewer than 8 points) because the element hasn't rendered yet.

- **Zero matches** — A role locator or selector that returns no elements is treated as transient, assuming the DOM will update.

- **Unknown reference ID** — When a ref points to a node not yet tracked, the system assumes it will arrive.

```typescript
// From src/element-resolver.ts — transient due to missing render
if (!boxModel || boxModel.content.length < 8) {
  throw new ElementResolutionError(
    "Element has no box model (not rendered or zero-sized)",
    "transient",
  );
}

```

### Permanent Failures: Abort Immediately

**Permanent failures** signal unrecoverable conditions where retrying would never succeed. The runtime propagates these immediately.

Common permanent scenarios include:

- **Invalid selector syntax** — A malformed selector string cannot be evaluated.

- **Multiple element matches** — When single-element resolution is required but the locator is ambiguous.

- **Missing backend DOM node ID** — AX nodes without `backendDOMNodeId` cannot be mapped to actual DOM elements.

```typescript
// Ambiguous match → permanent failure
if (/\bmatched \d+ elements\b/.test(message)) {
  return new ElementResolutionError(message, "permanent");
}
return new ElementResolutionError(
  `Invalid selector: ${selector}: ${message}`,
  "permanent",
);

```

## Runtime Behavior: Where the Distinction Matters

The `kind` property drives retry logic throughout ego-lite's driver layer.

### Poll-and-Retry Loops

In [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts), the wait helpers explicitly check `err.kind` before deciding whether to continue polling:

```typescript
// From src/driver/waits.ts lines 505-506
if (err instanceof ElementResolutionError && err.kind === "transient") {
  // Continue polling until timeout
} else {
  // Permanent error — abort immediately
  throw err;
}

```

This pattern prevents wasted cycles on unrecoverable errors while allowing legitimate timing issues to resolve naturally.

### Locator Fallbacks

[`src/driver/locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/locator.ts) uses the error kind to decide whether alternative resolution strategies should be attempted. A transient failure might trigger a different selector approach; a permanent one halts the chain.

## Key Files in the Resolution Pipeline

| File | Responsibility |
|------|----------------|
| [`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 implements all lookup logic with `kind` assignment. |
| [`package/ego-browser/src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/driver/waits.ts) | Implements retry loops that branch on `err.kind === "transient"`. |
| [`package/ego-browser/src/driver/locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/driver/locator.ts) | Consumes resolution errors to drive fallback and error-handling strategies. |

## Summary

- **ego-lite element resolution** returns failures categorized by recoverability, not just error messages.
- **Transient failures** (`kind: "transient"`) trigger automatic retry until success or timeout—ideal for race conditions with rendering.
- **Permanent failures** (`kind: "permanent"`) abort immediately since retrying would never help—used for invalid selectors and ambiguous matches.
- The `kind` property is checked in [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts) and [`src/driver/locator.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/locator.ts) to determine runtime behavior.

## Frequently Asked Questions

### What triggers a transient vs permanent error in ego-lite?

Transient errors occur when an element simply isn't available *yet*—missing box models, zero matches, or unknown refs. Permanent errors indicate fundamental problems: invalid selector syntax, multiple matches when one is required, or AX nodes lacking backend DOM IDs.

### How does ego-lite decide whether to retry a failed element lookup?

The runtime inspects `err.kind` on `ElementResolutionError`. If `kind === "transient"`, polling loops in [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts) continue until timeout. Any other `kind` aborts the operation immediately.

### Can I override the error kind in ego-lite's element resolver?

The `kind` is set internally based on failure mode detection in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts). Custom resolution logic would need to construct `ElementResolutionError` with the appropriate `kind` string to participate in the retry/abort protocol correctly.