garrytan/gstack · error · Error

Ref ${selector} (${entry.role} "${entry.name}") is stale — e

Error message

Ref ${selector} (${entry.role} "${entry.name}") is stale — element no longer exists. Run 'snapshot' for fresh refs.

What it means

Thrown by resolveRef when the ref IS present in refMap but `entry.locator.count()` returns 0 — the element was removed from the DOM after the snapshot was taken. The message includes the role and accessible name from the original snapshot so you can identify which element went stale.

Source

Thrown at browse/src/tab-session.ts:99

    this.refMap.clear();
  }

  /**
   * Resolve a selector that may be a @ref (e.g., "@e3", "@c1") or a CSS selector.
   * Returns { locator } for refs or { selector } for CSS selectors.
   */
  async resolveRef(selector: string): Promise<{ locator: Locator } | { selector: string }> {
    if (selector.startsWith('@e') || selector.startsWith('@c')) {
      const ref = selector.slice(1); // "e3" or "c1"
      const entry = this.refMap.get(ref);
      if (!entry) {
        throw new Error(
          `Ref ${selector} not found. Run 'snapshot' to get fresh refs.`
        );
      }
      const count = await entry.locator.count();
      if (count === 0) {
        throw new Error(
          `Ref ${selector} (${entry.role} "${entry.name}") is stale — element no longer exists. ` +
          `Run 'snapshot' for fresh refs.`
        );
      }
      return { locator: entry.locator };
    }
    return { selector };
  }

  /** Get the ARIA role for a ref selector, or null for CSS selectors / unknown refs. */
  getRefRole(selector: string): string | null {
    if (selector.startsWith('@e') || selector.startsWith('@c')) {
      const entry = this.refMap.get(selector.slice(1));
      return entry?.role ?? null;
    }
    return null;
  }

View on GitHub (pinned to 94993f7401)

Solutions

  1. Re-run `snapshot` to capture a fresh refMap against the current DOM.
  2. For SPAs, snapshot closer to the action (immediately after the wait-for that confirms the element is stable).
  3. Use a CSS selector targeting a stable attribute (data-testid) if the element re-render is intermittent.
  4. Wait for the element to settle (waitForSelector) before re-snapshotting.

Example fix

// before
await click(session, '@e3'); // element re-rendered → stale
// after
await page.waitForSelector('[data-testid=submit]');
await snapshot(session);
await click(session, '@e3');
Defensive patterns

Strategy: retry

Try / catch

async function clickStable(session: TabSession, sel: string) {
  for (let attempt = 0; attempt < 2; attempt++) {
    try { return await click(session, sel); }
    catch (e: any) {
      if (/is stale/.test(e.message)) {
        await snapshot(session); // re-resolve ref against fresh DOM
        continue;
      }
      throw e;
    }
  }
  throw new Error(`ref ${sel} stayed stale after re-snapshot`);
}

Prevention

When it happens

Trigger: The page mutated (SPA re-render, list virtualization, modal close, AJAX update) between the snapshot and the action; the element was in an iframe that detached; a CSS transition completed and the node was swapped.

Common situations: Single-page apps that re-render on every state change; clicking a ref on a list that re-virtualizes on scroll; clicking an element inside an iframe whose parent navigated.

Related errors


AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12). Data as JSON: /api/errors/cf025574fc64aa09. Report an issue: GitHub.