Next-Gen App & Browser Testing Cloud
Trusted by 2 Mn+ QAs & Devs to accelerate their release cycles

To debug failures in Cypress tests, start by reading the error message, code frame, and command log in the test runner, then pause execution where it breaks using .debug(), cy.pause(), or a debugger statement with DevTools open. For headless runs, inspect the failure screenshot and video, turn on verbose logs with DEBUG=cypress:*, use test retries to surface flakiness, and replay the full run with Test Replay in Cypress Cloud. The right tool depends on whether you are debugging interactively or after a CI run.
Cypress gives you a lot of context the moment a test fails. Before adding any breakpoints, read what the runner is already telling you. Most failures are solved here without writing a single debug line.
When the command log is not enough, pause the test where it breaks and inspect the live application, the DOM, the network, and your variables. Launch the interactive runner first:
npx cypress openThe .debug() command pauses the run and triggers a debugger in the browser, yielding the previous subject so you can inspect it in the DevTools console. You can chain it after any command or drop in cy.debug() mid-test. DevTools must be open for the breakpoint to trip.
it('logs in the user', () => {
cy.visit('/login')
cy.get('[data-cy=email]')
.type('[email protected]')
.debug() // inspect the email field's subject in DevTools
cy.debug() // pause here and inspect the page state
cy.get('[data-cy=submit]').click()
})cy.pause() stops the run at that point and lets you step through the remaining commands one at a time using the controls in the command log. It is ideal for confirming that each command has the desired effect, since you can inspect the web application, the DOM, the network, or any storage between steps.
it('adds an item to the cart', () => {
cy.visit('/products')
cy.pause() // run stops; resume or step command-by-command
cy.get('[data-cy=product-1]').click()
cy.get('[data-cy=add-to-cart]').click()
})A plain debugger statement works too, but because Cypress commands are queued and run asynchronously, you must place it inside a .then() callback where the JavaScript actually executes. Putting it between two chained commands will not break where you expect.
cy.get('[data-cy=total]').then(($total) => {
// $total is the real DOM element here
debugger // breaks with DevTools open
expect($total.text()).to.contain('$49.99')
})Logging is the fastest way to confirm what a command actually returned without stopping the run. Use cy.log() to write a message into the Cypress Command Log so it lines up with your test timeline, and console.log() to dump objects or large payloads into the browser console for expansion.
cy.get('[data-cy=user-name]').then(($el) => {
cy.log('User name text: ' + $el.text()) // shows in Command Log
console.log('Element:', $el) // expandable in browser console
})On a headless cypress run, there is no live runner to watch, so Cypress captures artifacts instead. A screenshot is taken automatically at the exact point of failure, and a video of the entire spec is recorded if you enable it. These are invaluable for understanding a CI failure you cannot reproduce locally.
Note that from Cypress 13 onward, video is off by default and must be turned on explicitly:
// cypress.config.js
const { defineConfig } = require('cypress')
module.exports = defineConfig({
video: true, // record a video of each spec
screenshotOnRunFailure: true, // default true
e2e: {
// ...your e2e config
},
})You can also capture a screenshot on demand at any step with cy.screenshot() to record state before a known-tricky action.
cy.get('[data-cy=checkout]').click()
cy.screenshot('after-checkout-click') // saved to cypress/screenshotsYou can also run a headless command in headed mode and keep the browser open afterward to inspect the final state:
npx cypress run --headed --no-exit --spec "cypress/e2e/login.cy.js"When a failure looks like an internal or environment issue rather than a test logic bug, enable Cypress's verbose debug output. Set the DEBUG environment variable before running, and scope it so you are not flooded with logs.
# macOS / Linux - all Cypress debug logs
DEBUG=cypress:* npx cypress run
# Narrow the scope to the server process
DEBUG=cypress:server:* npx cypress run
# Windows PowerShell
$env:DEBUG="cypress:*"; npx cypress runIf a test fails intermittently, enable test retries so Cypress automatically re-runs a failed test before marking it failed. This both stabilizes CI and helps you tell a genuine bug from a flaky one. Retries should buy you time to fix the root cause, not hide it.
// cypress.config.js
module.exports = defineConfig({
retries: {
runMode: 2, // retry up to 2 times in cypress run (CI)
openMode: 0, // no retries in cypress open
},
})Timing problems are the most common source of flakiness. For the underlying patterns, see How to Handle Asynchronous Actions in Cypress?.
Many failures are not UI bugs at all, they are requests that returned the wrong data or had not finished when an assertion ran. Use cy.intercept() to spy on or stub network traffic, then wait on the aliased request so the test only continues once the response has arrived.
cy.intercept('GET', '/api/cart').as('getCart')
cy.visit('/cart')
cy.wait('@getCart').then((interception) => {
// inspect the real request and response that the app received
console.log(interception.response.statusCode, interception.response.body)
})
cy.get('[data-cy=cart-count]').should('have.text', '2')Most Cypress failures fall into a handful of recurring categories. Matching the symptom to the cause points you straight at the fix.
| Cause | Typical Symptom | Fix |
|---|---|---|
| Async / timing | Element or value not ready, passes locally and fails in CI | Use retryable assertions instead of fixed cy.wait(ms); wait on cy.intercept() aliases |
| Detached DOM | "Element is detached from the DOM" error | Re-query inside the chain; avoid storing element refs; add a guard .should() |
| Flaky selectors | Selector breaks when markup or classes change | Prefer stable data-cy attributes over brittle CSS or XPath |
| Cross-origin navigation | Test errors after navigating to a different domain | Wrap the cross-origin steps in cy.origin() |
| Unstubbed external services | Random failures tied to a third-party API | Stub the response with cy.intercept() to remove external flakiness |
Local debugging works for one machine, but flaky failures in CI often only appear on a specific browser or OS. Test Replay in Cypress Cloud lets you replay a recorded run with the DOM, network, and console exactly as they were, instead of guessing from a screenshot. Record a run by passing your project's record key:
npx cypress run --record --key <your-record-key>To find browser-specific failures before they reach your users, you can run the same Cypress suite across a wide matrix of browsers and versions on the Cypress, then use the captured logs, screenshots, and video to root-cause each failure without rebuilding the environment yourself. Running across real browser and OS combinations surfaces the timing and rendering issues that a single local browser hides.
The debugger statement and .debug() only pause when the browser DevTools are open. Run cypress open, open DevTools in the spawned browser, then re-run the test. If you still don't break, make sure the debugger sits inside a .then() callback (synchronous JavaScript) rather than between two chained Cypress commands, which are only queued at that point.
cy.log() prints a message into the Cypress Command Log next to your commands, so it is tied to the test timeline. console.log() writes to the browser's developer console and is better for dumping objects, variables, or large payloads you want to expand and inspect.
On a headless cypress run, Cypress saves a screenshot at the point of failure and, if video is enabled, a recording of the whole spec. Inspect those artifacts first, enable DEBUG=cypress:* for verbose logs, and use Test Replay in Cypress Cloud to replay the run with the DOM, network, and console exactly as they were.
Random failures are almost always timing or flakiness issues. Replace fixed cy.wait(ms) calls with assertions that Cypress retries automatically, wait on the network with cy.intercept(), and re-query elements that the app re-renders. Enabling test retries buys stability while you find the root cause, but it should not replace the fix.
From Cypress 13 onward, video recording is off by default and must be enabled with video: true in cypress.config.js. Recording adds a small amount of overhead per spec, which is usually worth it in CI because the video often shows the failure cause without a re-run.
A "detached from the DOM" error means the element you queried was re-rendered before Cypress acted on it. Avoid storing element references in variables, re-query inside the same chain, and add a guard assertion such as .should('be.visible') so Cypress retries against the fresh element.
KaneAI - Testing Assistant
World’s first AI-Native E2E testing agent.

TestMu AI forEnterprise
Get access to solutions built on Enterprise
grade security, privacy, & compliance