Cypress: Element Detached from the DOM, and Why the Page Updated

10 min readupdated August 4, 2026

Your test clicks Save, then checks the row it just saved. Cypress stops:

cy.click() failed because the page updated as a result of this command,
but you tried to continue the command chain.

Or the shorter version people quote:

The element is detached from the DOM.

Nothing is broken in your application. Cypress is describing something real: it was holding a specific element, and the page replaced it. The element in its hand no longer exists on the page.

The fix is not a longer wait. It is understanding what Cypress is holding on to.

What "detached" actually means

When Cypress finds an element, the result becomes the subject of the chain. That subject is a reference to one specific DOM element, the exact object that was in the page at that moment.

Then something re-renders. React, Vue, or Angular does not usually edit the element you had. It throws it away and builds a new one that looks the same. Your ticket now points at an element that is no longer attached to the document.

Cypress notices, and refuses to act on a dead element rather than doing something meaningless.

This is why it happens far more on modern framework applications than on old server-rendered pages. Re-rendering is normal there, and every re-render detaches whatever was in that part of the tree.

The rule that explains it: queries retry, commands do not

This is the part worth learning properly, because it turns a confusing error into a predictable one.

Queries find things. cy.get(), .find(), .contains(), .parent(), .first(). Cypress re-runs the whole chain of queries when it retries, so they always work from a fresh look at the page.

Commands do things. .click(), .type(), .select(), .check(). A command acts once. It cannot be retried, because clicking twice is not the same as clicking once.

Assertions check things. .should(). They retry, and they re-run the queries in front of them.

So the danger is precise: anything chained after a command is stuck with a subject that was found before the command ran. If the command changed the page, that subject may be dead.

// Risky: .should() here is chained after a command that re-rendered the row
cy.get('[data-testid="row"]').click().should('have.class', 'selected');

The fix: break the chain after a command

The official guidance, and the one that fixes most cases, is simply to stop chaining after a command. Start again with a fresh query.

// Instead of this
cy.get('[data-testid="save"]').click().should('be.disabled');

// Do this
cy.get('[data-testid="save"]').click();
cy.get('[data-testid="save"]').should('be.disabled');

The second cy.get runs after the update and finds whatever is in the page now. It costs one extra line and removes the entire class of problem.

The same applies inside a list:

// Fragile: the row is found, clicked, and the list re-renders underneath
cy.get('[data-testid="row"]').first().click().find('.status').should('have.text', 'Saved');

// Stable: query again after the update
cy.get('[data-testid="row"]').first().click();
cy.get('[data-testid="row"]').first().find('.status').should('have.text', 'Saved');

Do not fix it with cy.wait

The tempting fix looks like this:

cy.get('[data-testid="row"]').click();
cy.wait(1000); // please do not
cy.get('.status').should('have.text', 'Saved');

It often makes the error go away, which is exactly why it is dangerous. It hides the real issue behind a delay, wastes a second on every run, and still fails on a slow day. If the page re-renders twice, a fixed wait can land you in the gap between them.

Break the chain instead. It is faster and it is correct.

The alias trap

Aliases feel like they should help. Sometimes they make it worse:

cy.get('[data-testid="row"]').as('row');
cy.get('@row').click();
cy.get('@row').should('have.class', 'selected'); // may be detached

When you alias a DOM element, Cypress remembers the element. If the page re-renders, that alias can point at the old one.

Aliasing a selector rather than an element avoids it. The simplest reliable version is to just query again, which is what the previous section does.

When the page re-renders repeatedly

Some applications re-render several times as data arrives: a skeleton, then partial data, then the final state. A query can resolve during one of those intermediate renders and be detached a moment later.

The fix is to wait for the settled state rather than the first thing that matches. Assert on the condition that only holds at the end:

// Wait for the loading state to be gone before interacting.
cy.get('[data-testid="spinner"]').should('not.exist');
cy.get('[data-testid="row"]').should('have.length', 3);
cy.get('[data-testid="row"]').first().click();

Assertions retry, so each of those lines waits for the real condition. Because .should() re-runs the query in front of it, you get a fresh element every attempt.

How this compares to Selenium

If you have used Selenium, this is the same idea as StaleElementReferenceException. Both tools hand you a reference to a real element, and both break when the page replaces it.

The difference is what saves you. Cypress re-runs queries automatically when it retries, so the fix is usually to let it query again by breaking the chain. In Selenium you re-find the element yourself.

Playwright avoids the problem entirely, because a locator is a description that is looked up on every use rather than a captured element. That design difference is covered in Playwright vs Selenium.

The habit worth building

When you see this error, do not ask "how long should I wait". Ask "what did I do that changed the page, and am I still holding something from before it?"

In practice that becomes one rule: after any command that changes the page, start a new chain. Follow it and this error mostly disappears from your suite.

Remember this

Cypress holds a specific element as the subject of a chain, and a re-render throws that element away. Queries and assertions retry, commands do not, so anything chained after a command can be stuck with a dead element. Break the chain and query again rather than adding a wait, be careful aliasing elements, and assert the page has settled before interacting with it.

The Cypress track goes deeper on the retrying that all of this rests on: how retrying really works.

Related: element is being covered by another element and how to fix flaky tests.

Reference: Cypress error messages.

now prove it

Prove a contact was actually saved

Reading is the easy part. Write the test yourself, then we break the app on purpose to check it would really catch the bug.

Solve this problem →

Keep reading