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

  1. Use Locators instead of ElementHandle — Locators re-resolve the element on every action and stay valid across re-renders.
  2. Re-query the element handle immediately before the action if you must use handles.
  3. Await stability (waitFor selector / waitForLoadState) so re-renders settle before acting.
  4. 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

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


AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12). Data as JSON: /api/errors/141dc72996d8401f. Report an issue: GitHub.