microsoft/playwright · error · Error
Element is not attached to the DOM
Error message
Element is not attached to the DOM
What it means
An element handle references a DOM node that has since been removed from the document (the injected utility returned 'error:notconnected'). throwElementIsNotAttached converts that into Error('Element is not attached to the DOM'). ElementHandle is a snapshot pointer; once the node is removed/re-rendered the handle is stale and any action on it fails.
Source
Thrown at packages/playwright-core/src/server/dom.ts:971
// Hit target in the parent frame should hit the child frame element.
const hitTargetResult = await progress.race(element.evaluateInUtility(([injected, element, hitPoint]) => {
return injected.expectHitTarget(hitPoint, element);
}, point));
if (hitTargetResult !== 'done')
return hitTargetResult;
}
return { framePoint: data[0].pointInFrame };
}
}
export function throwRetargetableDOMError<T>(result: T | 'error:notconnected'): T {
if (result === 'error:notconnected')
throwElementIsNotAttached();
return result;
}
export function throwElementIsNotAttached(): never {
throw new Error('Element is not attached to the DOM');
}
export function assertDone(result: 'done'): void {
// This function converts 'done' to void and ensures typescript catches unhandled errors.
}
function roundPoint(point: types.Point): types.Point {
return {
x: (point.x * 100 | 0) / 100,
y: (point.y * 100 | 0) / 100,
};
}
function quadToRect(quad: types.Quad): types.Rect {
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;View on GitHub (pinned to c8fc3bf8d3)
Solutions
- Use Locators instead of ElementHandle — Locators re-resolve the element on every action and stay valid across re-renders.
- Re-query the element handle immediately before the action if you must use handles.
- Await stability (waitFor selector / waitForLoadState) so re-renders settle before acting.
- Avoid caching handles from beforeEach across the test body.
Example fix
// before — stale handle
const handle = await page.$('#item');
await page.reload();
await handle.click(); // node gone → throws
// after — locator re-resolves
const item = page.locator('#item');
await page.reload();
await item.click(); Defensive patterns
Strategy: retry
Validate before calling
// Use a Locator so the element is re-resolved on each action.
const item = page.locator('#item');
await page.reload();
await item.click(); // re-queries after reload Try / catch
// If you must use a handle, re-query on staleness.
async function clickFresh(page: Page, selector: string) {
try {
await (await page.$(selector))?.click();
} catch (e) {
if (e.message.includes('not attached to the DOM')) {
await (await page.$(selector))?.click(); // re-query and retry once
} else throw e;
}
} Prevention
- Prefer Locators over ElementHandle — they re-resolve automatically.
- Do not cache element handles across navigations or re-renders.
- Await load-state/stability before acting to let re-renders settle.
When it happens
Trigger: Querying an ElementHandle, then after a re-render or navigation the node is replaced, then calling an action on the old handle; React/Vue re-rendering a list item so the old node is discarded; SPA route change that replaces the container.
Common situations: Storing element handles across awaits in React/Vue apps; acting on a handle obtained before an animation that swaps nodes; navigating away and back so the original nodes are new instances.
Related errors
- Unable to adopt element handle from a different document
- The object has been collected to prevent unbounded heap grow
- Cannot set input files to detached element
- Unable to adopt element handle from a different document
- Frame has been detached.
AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12).
Data as JSON: /api/errors/141dc72996d8401f.
Report an issue: GitHub.