Skip to content
Playwright · error message

Playwright: Element is not attached to the DOM

RetryableDetached elements, frames & contexts

Last verified

The exact message
Error: elementHandle.click: Element is not attached to the DOM

What it means

You are holding a handle to a specific DOM node, and that node has since been removed from the document. Playwright cannot click, fill or read something that is no longer on the page, so the call fails. This is Playwright's equivalent of Selenium's stale element error.

It occurs with ElementHandle objects, which you get from page.$(), page.$$(), page.waitForSelector() or locator.elementHandle(). A handle points at one node forever; when a framework such as React or Vue re-renders a list, it may replace that node with an identical-looking new one, and your handle is left pointing at the old one.

Locators do not have this problem: a Locator re-queries the page on every action, and if the element is detached mid-action Playwright logs element was detached from the DOM, retrying and tries again. That is why the official docs discourage element handles.

What it looks like

// Fragile: handles go stale when the list re-renders
const rows = await page.$$('.todo-item');
for (const row of rows) await row.click();

// Robust: locator re-queries every time
const items = page.locator('.todo-item');
const n = await items.count();
for (let i = 0; i < n; i++) {
  await items.nth(i).click();
}

Why it happens

  • Code stores an ElementHandle, then something re-renders or navigates before it is used.
  • Looping over page.$$() results while each click changes the list (pagination, delete buttons, expanding rows).
  • A single-page app swaps the route content after a client-side navigation.
  • Legacy code ported from Puppeteer that relies on page.$ and handle.click().

How to fix it

  1. Replace page.$() / page.waitForSelector() handles with locators: page.locator(), getByRole() and friends.
  2. When iterating a changing list, re-locate each item by index or text inside the loop instead of reusing a pre-fetched array.
  3. Wait for the re-render to finish before acting, for example with expect(locator).toHaveCount(n) or toBeVisible().
  4. If you must use a handle, obtain it immediately before the action, not earlier in the flow.

How to stop it recurring

Adopt locators everywhere and lint against page.$, page.$$ and elementHandle in new code. Locators carry the selector rather than the node, so re-renders are invisible to your script. When a step mutates the list you are iterating, iterate over stable identifiers (text, IDs) instead of positions.

Official documentation: playwright.dev/docs/api/class-elementhandle. See all Playwright error messages.

Related errors

Definitions

Quick answers

What does "Element is not attached to the DOM" mean in Playwright?

You are holding a handle to a specific DOM node, and that node has since been removed from the document. Playwright cannot click, fill or read something that is no longer on the page, so the call fails. This is Playwright's equivalent of Selenium's stale element error. It occurs with ElementHandle objects, which you get from page.$() , page.$$() , page.waitForSelector() or locator.elementHandle() .

How do I fix "Element is not attached to the DOM"?

1. Replace page.$() / page.waitForSelector() handles with locators: page.locator(), getByRole() and friends. 2. When iterating a changing list, re-locate each item by index or text inside the loop instead of reusing a pre-fetched array. 3. Wait for the re-render to finish before acting, for example with expect(locator).toHaveCount(n) or toBeVisible(). 4. If you must use a handle, obtain it immediately before the action, not earlier in the flow.

Run browser automation you can watch and take over

Autonoly runs each job in a real cloud Chromium browser driven by Playwright. Timeouts and detached elements still happen; the difference is that you can watch the run live, see the step that failed, and take control of the browser to finish it.

Also see Live browser control.