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
- Re-run `snapshot` to capture a fresh refMap against the current DOM.
- For SPAs, snapshot closer to the action (immediately after the wait-for that confirms the element is stable).
- Use a CSS selector targeting a stable attribute (data-testid) if the element re-render is intermittent.
- 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
- Wait for the element to settle (waitForSelector) before snapshotting.
- For SPAs, snapshot immediately before each action rather than batching.
- Use CSS selectors on data-testid for elements that re-render.
- Treat stale-ref as recoverable — re-snapshot and retry once.
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
- Ref ${selector} not found. Run 'snapshot' to get fresh refs.
- Invalid file URL: file:/// has no path. Use file:///<absolut
- Invalid file URL: ${url}. Use file:///<absolute-path> or fil
- Invalid file URL: file:// is empty. Use file:///<absolute-pa
- Invalid URL: ${url}
AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12).
Data as JSON: /api/errors/cf025574fc64aa09.
Report an issue: GitHub.