# How the Element Resolver Distinguishes Between Transient and Permanent Resolution Failures in ego-lite

> Discover how ego-lite's element resolver differentiates transient from permanent resolution failures using the kind field on ElementResolutionError. Learn the key indicators for retryable vs non-retryable errors.

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

---

**The element resolver uses a `kind` field on `ElementResolutionError` to classify failures as `"transient"` (retryable) or `"permanent"` (non-retryable), with the distinction determined by failure type, selector evaluation results, and ref-ID validity.**

The **element resolver** in `ego-lite` converts CSS selectors or ref-IDs (like `@21`) into concrete element coordinates or object IDs. When resolution fails, the system must decide whether retrying could succeed. This article examines how `ego-lite` implements this classification based on the source code in `citrolabs/ego-lite`.

## The `kind` Field: Core Mechanism

Every `ElementResolutionError` carries a **`kind`** property with two possible values:

- **`"transient"`** — The failure may resolve on retry (e.g., stale ref, missing snapshot data)
- **`"permanent"`** — The failure will persist regardless of retries (e.g., invalid selector, ambiguous matches)

Callers inspect this field to implement appropriate retry logic or immediate error surfacing.

## How Resolution Failures Are Classified

The resolver applies four distinct rules to determine `kind`, implemented in [`element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/element-resolver.ts).

### Multiple Selector Matches: Permanent Failure

When a selector matches more than one element, the resolver cannot determine user intent. The helper `matchCountKind()` parses error messages for patterns like "matched 3 elements" and returns **`"permanent"`** when the count exceeds 1.

```typescript
// element-resolver.ts lines 46-50
if (matchCount > 1) {
  return "permanent";  // Cannot disambiguate multiple matches
}
return "transient";    // Zero matches might resolve after DOM changes

```

This conservative approach treats ambiguity as unrecoverable.

### Invalid Selector Syntax: Permanent Failure

Evaluation errors that don't match the "matched N elements" pattern indicate broken selector syntax or runtime exceptions. The `selectorResolutionError()` wrapper unconditionally assigns **`kind: "permanent"`**.

```typescript
// element-resolver.ts lines 52-60
function selectorResolutionError(originalError: Error): ElementResolutionError {
  // Parse message for match count to determine if retryable
  const kind = matchCountKind(originalError.message);
  return new ElementResolutionError(
    `Selector resolution failed: ${originalError.message}`,
    kind === "transient" ? "transient" : "permanent"
  );
}

```

Syntax errors cannot be fixed by waiting—the selector itself must change.

### Missing or Stale Ref-ID: Transient Failure

Ref-IDs reference cached backend nodes that may become invalid after DOM mutations. The resolver throws **`"transient"`** because a fresh snapshot can repopulate the ref map.

```typescript
// element-resolver.ts lines 74-75
throw new ElementResolutionError(`Unknown ref: ${refId}`, "transient");

```

This enables automatic recovery through snapshot refresh cycles.

### Unavailable Box Model: Transient Failure

When a node exists but lacks render geometry (not yet displayed or hidden), the error propagates unchanged with **`"transient"`** status. Lines 94-99 in `resolveElementCenter` preserve this classification, allowing retries after navigation or visibility changes.

## Practical Implementation Patterns

### Basic Error Handling with Conditional Retry

```javascript
try {
  const { x, y } = await ego.resolveElementCenter(selectorOrRef);
  // Use coordinates...
} catch (e) {
  if (e instanceof ego.ElementResolutionError && e.kind === "transient") {
    await ego.waitForIdle(0.5);
    return await ego.resolveElementCenter(selectorOrRef);  // Retry once
  }
  // Permanent: surface to user
  console.error("Resolution failed permanently:", e.message);
  throw e;
}

```

### Robust Multi-Attempt Resolution for Stale Refs

```javascript
async function safeResolveRef(ref, maxAttempts = 3) {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await ego.resolveElementObjectId(ref);
    } catch (e) {
      if (e.kind === "transient") {
        await ego.takeSnapshot();  // Force ref map refresh
        continue;
      }
      throw e;  // Permanent failure aborts immediately
    }
  }
  throw new Error(`Failed to resolve ref after ${maxAttempts} attempts`);
}

```

## Source File Reference

| File | Location | Purpose |
|:---|:---|:---|
| [`element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/element-resolver.ts) | `package/ego-browser/src/` | Core resolution logic with `kind` assignment |
| `element-resolver.test.mjs` | `package/ego-browser/src/` | Test verification of transient/permanent behavior |

The implementation ensures that retry resources are spent only on failures where DOM state changes could yield success, while permanent configuration issues receive immediate, clear feedback.

## Summary

- **`ElementResolutionError.kind`** enables programmatic distinction between retryable and fatal resolution failures
- **Permanent failures**: multiple selector matches, invalid syntax, evaluation exceptions
- **Transient failures**: stale ref-IDs, missing snapshot data, unavailable box models
- The `matchCountKind()` helper parses error messages to disambiguate zero matches (transient) from multiple matches (permanent)
- Callers should implement **exponential backoff with snapshot refresh** for transient errors and **immediate user notification** for permanent errors

## Frequently Asked Questions

### What happens if I retry a permanent resolution failure?

Retrying a permanent failure wastes resources and delays error reporting. The same invalid selector or ambiguous match will persist across all attempts. The `ego-lite` source code guarantees this through the hard-coded `"permanent"` assignment in `selectorResolutionError()`.

### How does the resolver handle zero selector matches versus multiple matches?

Zero matches receive `"transient"` classification because the DOM may eventually contain the target element. Multiple matches receive `"permanent"` because the resolver cannot determine which matching element the caller intended. The `matchCountKind()` function implements this split at lines 46-50.

### Can transient failures become permanent?

No—a transient classification never converts to permanent within a single resolution attempt. However, repeated transient failures may exhaust caller retry limits, causing the operation to fail with a timeout or attempt-exceeded error rather than a resolution error.

### Where should I implement retry logic for transient errors?

Implement retry loops in **calling code** rather than inside the resolver. The resolver remains stateless and deterministic, leaving retry policy (attempt count, delays, snapshot triggers) to application-specific handlers. The `safeResolveRef()` example above demonstrates this pattern.