QA How-To
How to Debug a failing test in VS Code in Cypress (2026)
Learn cypress how to debug a failing test in VS Code using open mode, DevTools, breakpoints, network evidence, source maps, and focused local and CI reruns.
23 min read | 3,292 words
TL;DR
Open the failing spec with `npx cypress open`, narrow it with `.only`, and inspect the failed command before editing. Cypress test code executes in the browser, so use VS Code to open and change source files, then use the launched browser's DevTools with `.debug()`, `cy.pause()`, or a `debugger` inside `.then()` to inspect runtime state.
Key Takeaways
- Run the smallest failing spec in Cypress open mode before changing code.
- Cypress spec code runs in the browser, so use browser DevTools for test breakpoints and VS Code for navigation and editing.
- Place `debugger` inside a Cypress callback, or use `.debug()` and `cy.pause()` with DevTools already open.
- Use the Command Log, DOM snapshots, console, and Network panel as one evidence set.
- Replace fixed waits with observable application conditions instead of masking the failure.
- Reproduce CI-only failures with the same browser, viewport, environment, data, and run mode.
- Remove temporary focus markers and prove the fix with repeated isolated and suite-level runs.
If you searched for cypress how to debug a failing test in VS Code, start with one important distinction: VS Code is your source editor and test launcher, but Cypress spec code executes in the browser. Use VS Code to navigate to the failing line, make focused changes, and rerun the spec. Use the Cypress Command Log and the launched browser's Developer Tools to pause and inspect the browser-side test.
A good debugging session is an evidence pipeline, not a sequence of guesses. Reproduce the smallest failure, identify whether the broken contract is setup, action, network, rendering, or assertion, inspect the state at that boundary, and change only the cause. This guide shows that workflow with current Cypress APIs and explains the common breakpoint misconception that wastes the most time.
TL;DR
| Need | Best tool | What it proves |
|---|---|---|
| Open the responsible source line | Cypress error link plus VS Code | Exact spec, line, and code frame |
| Inspect command subjects | Cypress Command Log and .debug() |
What a query or command yielded |
| Stop between Cypress commands | cy.pause() |
Application state before the next queued command |
| Stop inside executed test logic | debugger inside .then() |
Live browser scope, DOM, and JavaScript values |
| Diagnose request timing or payload | cy.intercept(), aliases, Network panel |
Which request occurred and what it carried |
| Diagnose CI-only behavior | Headed rerun with matching run inputs | Whether browser, data, or environment changes the result |
The short workflow is: open the spec, run only the failing test, read the first useful error, inspect the previous command, pause at the failing boundary, form one hypothesis, change one cause, and rerun enough times to challenge the fix.
1. Cypress How to Debug a Failing Test in VS Code: Understand the Runtime
The most important mental model is that VS Code does not directly execute the browser portion of a Cypress test. Cypress bundles your TypeScript or JavaScript spec, then runs it with the application in a browser. Attaching VS Code's Node debugger to a normal spec therefore does not make browser-side breakpoints bind. Open the browser DevTools for .debug(), cy.pause(), and debugger statements in spec callbacks.
VS Code still has a central role. Configure Cypress as the file opener in the Cypress App, click the source link in an error or Command Log entry, and VS Code opens the responsible file at the reported line. IntelliSense, type checking, ESLint, search, Git history, and focused editing all shorten diagnosis. A Cypress test explorer extension can be convenient, but it is not required for this workflow and community extensions should be evaluated for compatibility.
There is a second runtime: Node code in setupNodeEvents and handlers registered for cy.task(). A Node debugger can inspect that code. Keep the boundary explicit. A failure in a task that reads a database is Node-side. A failure locating a button is browser-side. A test may cross both boundaries, but the debugger must attach to the runtime that owns the failing code.
If VS Code shows a hollow breakpoint in a spec while Cypress continues running, do not assume Cypress ignored the source map. You are probably asking a Node debugging session to stop browser code. Move the inspection to browser DevTools or add a Cypress-aware pause.
2. Reproduce the Smallest Failure Before Editing
A failure you cannot reproduce consistently is not yet ready for a code change. Start the Cypress App and choose the same testing type and browser implicated by the failure:
npx cypress open
Open the failing spec and temporarily focus the relevant test:
describe('invoice approval', () => {
it.only('approves a pending invoice', () => {
cy.visit('/invoices/inv_123')
cy.get('[data-cy=approve-invoice]').click()
cy.get('[data-cy=invoice-status]').should('have.text', 'Approved')
})
})
Use .only as a local diagnostic marker, never as a permanent fix. Confirm the application server, feature flags, seeded record, account role, viewport, and browser match the original run. If the test passes locally but failed in CI, preserve that difference instead of immediately weakening the assertion. Run the exact spec in run mode too:
npx cypress run --spec cypress/e2e/invoice-approval.cy.ts --browser chrome
A spec that passes in open mode but fails in run mode may depend on focus, timing, test order, cached state, animation, resource availability, or configuration. A test that fails only after another test likely has an isolation leak. A test that fails alone likely contains its own setup, synchronization, selector, or product defect. This simple comparison sharply reduces the search space.
Record the precise failure signature: assertion text, selector, requested URL, status, screenshot, attempt number, and first meaningful stack frame. Do not call every intermittent failure a timeout. Timed out retrying describes the observation deadline, not the root cause.
3. Read the Cypress Error and Command Log as a Timeline
Cypress errors usually contain the failed assertion, the expected and actual values, a code frame, and a stack trace. Start with the first frame in your code, not with the deepest framework frame. Click the source link to open it in VS Code, then read upward through the Command Log to reconstruct the state transition that should have produced the assertion.
The Command Log is more than a list. Click a command to inspect its console properties and DOM snapshot. Compare the snapshot before an action with the snapshot after it. Ask four questions:
- Did setup create the intended state?
- Did the action target the correct current element?
- Did the expected request and response occur?
- Did the application render the expected outcome?
Suppose invoice-status remained Pending. The assertion may be correct. The actual defect could be a click blocked by an overlay, a 403 response, a response body the frontend rejected, a delayed subscription update, or a stale invoice created by another test. Increasing defaultCommandTimeout would only delay all these outcomes.
Command snapshots are time-travel aids, not a live frozen browser. When you click an old command, the displayed snapshot represents Cypress's captured DOM around that command, while the Console and current page can reflect later state. Use a breakpoint when you need live JavaScript objects, active event listeners, storage, or request timing at a specific moment.
For intermittent behavior, compare the last passing command across multiple attempts. The point where timelines diverge is often more valuable than the final repeated error.
4. Use .debug(), cy.pause(), and debugger Correctly
These three tools stop at different boundaries. Keep browser DevTools open before expecting .debug() to hit a breakpoint.
it('shows the saved profile name', () => {
cy.visit('/profile')
cy.get('[data-cy=display-name]')
.debug()
.should('have.value', 'Asha Rao')
})
.debug() yields the same subject it received, so the assertion can continue. In DevTools, inspect the exposed subject, element attributes, and surrounding DOM. Use cy.debug() at the start of a chain when you need a pause before later commands.
cy.pause() is designed for stepping through queued commands:
cy.visit('/checkout')
cy.pause()
cy.get('[data-cy=place-order]').click()
cy.get('[data-cy=confirmation]').should('be.visible')
A raw debugger placed between queued Cypress commands usually runs while commands are only being scheduled. Put it inside a callback that Cypress executes after the required subject resolves:
cy.get('[data-cy=cart-total]').then(($total) => {
debugger
const text = $total.text().trim()
expect(text).to.eq('$80.00')
})
Now the breakpoint has a live $total in scope. Use .then() for diagnosis, not as an automatic replacement for a retryable .should(). A JavaScript assertion in .then() runs once. If the value is expected to settle over time, keep the final production assertion in .should() after debugging. Remove pauses and debugger statements when the cause is understood.
5. Debug Selectors, Detached Elements, and Retry Boundaries
Selector failures often indicate a state problem, not merely a spelling mistake. Inspect whether the element exists under another route, inside a dialog, behind a loading state, or after a rerender. Prefer stable attributes and user-visible meaning over generated CSS classes. The Cypress data-cy selector guide covers durable selector contracts.
A common failure chains through an application rerender:
cy.get('[data-cy=country]').select('India')
cy.get('[data-cy=state]').should('be.enabled').select('Karnataka')
The second fresh query is intentional. Selecting the country can replace the state control. Holding a jQuery element across that boundary risks acting on detached DOM. In DevTools, compare element.isConnected, inspect the current DOM, and look for framework rerenders after the action.
Cypress retries queries and linked assertions, but action commands execute once. Debug the chain in those terms. cy.get(...).find(...).should(...) can retry linked queries. A click is a state-changing boundary. After the click, query again for the result. The Cypress retry-ability examples explain why this is different from repeating the entire test.
If .should(callback) is involved, remember that its callback can run repeatedly. Do not click, write data, or enqueue Cypress commands inside it. Keep diagnostic logging repeat-safe, or use .then() after a retryable precondition. A failure that disappears when commands are broken into fresh queries often reveals a stale-subject assumption rather than a need for a longer timeout.
6. Debug Network-Driven Failures With Operation Evidence
Register a narrow intercept before the application can send the request. Alias it, trigger the user action, inspect the completed interception, then assert the visible result separately.
it('approves a pending invoice', () => {
cy.intercept('PATCH', '/api/invoices/inv_123').as('approveInvoice')
cy.visit('/invoices/inv_123')
cy.get('[data-cy=approve-invoice]').click()
cy.wait('@approveInvoice').then(({ request, response }) => {
expect(request.body).to.deep.eq({ status: 'approved' })
expect(response?.statusCode).to.eq(200)
})
cy.get('[data-cy=invoice-status]').should('have.text', 'Approved')
})
This test localizes three possible failures: the request never matched, the transport contract was wrong, or the UI failed to render a successful result. Click the intercept entry in the Command Log and inspect request headers, body, response, and duration. Use the browser Network panel for application requests and caching details, but remember that cy.request() executes from Cypress outside the browser and does not appear there.
Avoid intercepts such as cy.intercept('/api/*') when several operations could match. An early unrelated request may satisfy the alias and produce confusing evidence. For GraphQL, inspect operationName and alias the intended operation. For more patterns, see Cypress cy.intercept route examples.
Do not stub the response while diagnosing a suspected integration failure unless the comparison is deliberate. First observe the real response. Then a controlled stub can answer a specific question, such as whether the UI succeeds with a valid contract. That is an experiment, not proof that the backend works.
7. Inspect Console Errors, Storage, Cookies, and Application State
At a live breakpoint, inspect more than the selected element. The DevTools Console can show uncaught exceptions, failed resource loads, warnings, current location, feature flag state, cookies visible to JavaScript, and local or session storage. The Application panel provides a clearer view of browser storage and service workers.
If authentication unexpectedly redirects, inspect the actual path and cookie scope rather than adding another login command. If a feature is absent, check whether the flag request completed and whether the app cached a previous value. If a component is blank, look for an exception before assuming the selector is wrong.
You can preserve targeted console evidence with a stub created before application code loads:
it('does not log an application error while saving', () => {
cy.visit('/profile', {
onBeforeLoad(win) {
cy.stub(win.console, 'error').as('consoleError')
},
})
cy.get('[data-cy=save-profile]').click()
cy.get('[data-cy=save-status]').should('have.text', 'Saved')
cy.get('@consoleError').should('not.have.been.called')
})
Use this assertion only when the product contract truly forbids console errors in the scenario. Some frameworks emit known messages in development, and asserting globally can turn unrelated noise into test ownership. Never solve an application exception by broadly returning false from uncaught:exception. That hides potentially serious defects. If a narrowly documented third-party exception must be filtered, match its exact signature and keep an owner and removal condition.
8. Diagnose Timeouts and Flake Without Fixed Sleeps
A fixed cy.wait(5000) changes elapsed time but provides no evidence about the condition. It can make a race rarer, slow every passing run, and still fail under different load. Find the signal the user workflow actually depends on: a specific response, visible state, route change, enabled control, completed job, or deterministic clock.
Compare these approaches:
| Symptom | Weak change | Diagnostic replacement |
|---|---|---|
| Table is empty after load | cy.wait(3000) |
Alias the rows request, then assert rows |
| Button is covered | { force: true } |
Inspect overlay and wait for the real ready state |
| Text appears late | Increase every timeout | Keep a retryable text assertion with a justified local timeout |
| Debounce is slow | Sleep for debounce plus buffer | Use cy.clock() and cy.tick() if timers are controllable |
| Test passes on retry | Enable more retries | Compare attempts and fix data or synchronization |
Cypress test retries are useful for measuring intermittent behavior and preserving attempts, but a passed retry is still evidence of instability. Inspect screenshots, videos, network logs, and attempt-specific state. Read Cypress handling flaky tests for isolation and retry governance.
When changing a timeout, name the accepted latency. A ten-second local timeout for an approved asynchronous report may be valid. A global timeout increase because one selector is wrong is not. Debug whether the condition eventually becomes correct, never becomes correct, or was already missed before the intercept or listener registered.
9. Reproduce CI-Only Failures From VS Code
Start with the CI command and inputs, not with an approximate local click-through. Match the Cypress version, Node version used by configuration code, browser family and version, viewport, environment variables, locale, timezone, feature flags, spec order, shard, and data setup. Run the failing spec in run mode from VS Code's integrated terminal. If needed, add --headed to observe it without changing the basic mode:
npx cypress run \
--headed \
--browser chrome \
--spec cypress/e2e/invoice-approval.cy.ts
Do not copy secrets into the command or commit them to a VS Code launch file. Use the same approved secret injection method as CI. Confirm the application URL and build are identical. A local development server with source maps and a CI production build can expose different timing and minification behavior.
Download the original failure artifacts before rerunning if CI retention is short. The screenshot answers what was rendered at failure. Video shows the transition. The reporter gives the assertion and stack. Server logs or correlation IDs explain backend behavior. A Cypress Cloud replay, when the organization uses it, can add command and application context, but the core reasoning process remains the same.
If parallel execution is involved, use unique data and accounts. A passing isolated rerun does not disprove collision. Recreate the competing operation or run the same shard pattern. CI-only flake is often an environmental contract that the local reproduction did not yet include.
10. Debug Node-Side setupNodeEvents and cy.task() Failures
Cypress configuration and task handlers execute in Node, not in the application browser. The error usually mentions a task name, configuration evaluation, file access, database client, or a returned value. Add focused logging or a Node breakpoint in the handler and launch Cypress under a Node debugging setup appropriate to your project. VS Code's JavaScript debugger can inspect this runtime.
Keep tasks small and make failures explicit:
import { defineConfig } from 'cypress'
import fs from 'node:fs/promises'
export default defineConfig({
e2e: {
setupNodeEvents(on) {
on('task', {
async readReport(path: string) {
const text = await fs.readFile(path, 'utf8')
return JSON.parse(text)
},
})
},
},
})
it('reads a generated report', () => {
cy.task('readReport', 'tmp/report.json').then((report) => {
expect(report).to.have.property('status', 'complete')
})
})
A task must not resolve to undefined. Return a serializable value or null. Check paths relative to the Cypress project root, file permissions, ESM versus CommonJS imports, environment availability, and whether parallel workers share the same file. Do not debug a task by searching the browser Network panel, since it is Node work.
Use Cypress cy.task examples when the browser needs a controlled bridge to the filesystem or another Node-only capability. Do not move ordinary browser assertions into tasks simply to make breakpoints bind in VS Code. Preserve the correct test boundary.
11. Cypress How to Debug a Failing Test in VS Code: Prove the Fix
A fix is not proven when one watched rerun passes. First rerun the focused test enough times to challenge the original failure mode. Then remove .only, pauses, debugger statements, temporary logs, forced clicks, experimental stubs, and oversized timeouts. Run the complete spec to expose local state leakage, followed by the relevant suite or CI job.
Review the diff as a causal claim. If the failure was a request alias registered too late, the fix should register it before the trigger and assert the right operation. If the failure was shared data, the fix should generate isolated data and clean it through an approved boundary. If the product returned a 500, the test may need no change at all. The correct outcome can be a product fix with the original assertion preserved.
Capture what you learned in durable code: a clearer selector, a focused intercept, deterministic setup, a business assertion, or improved failure message. Avoid leaving diagnostic complexity that future readers must interpret.
A useful final check is to deliberately break the expected outcome once. Confirm the test fails for the intended reason and points to a meaningful line. This guards against a false-positive repair such as a weakened assertion, an intercept that matches the wrong request, or a conditional branch that skips the behavior. Restore the correct code, run the normal verification, and let CI test the same causal fix under its real conditions.
Interview Questions and Answers
Q: Why does a VS Code breakpoint in a Cypress spec remain unbound?
Cypress spec code runs in the launched browser, while a standard VS Code JavaScript debug session often targets Node. I use browser DevTools for spec breakpoints and VS Code for source navigation and editing. VS Code's Node debugger is appropriate for setupNodeEvents and cy.task() handlers.
Q: Where should you place a raw debugger statement?
I place it inside a callback that Cypress executes after the required command resolves, commonly .then(($el) => { debugger }). A statement between queued commands can run during command scheduling, before the browser state I need exists. I keep the final retry-dependent assertion in .should() after diagnosis.
Q: What is the difference between .debug() and cy.pause()?
.debug() inserts a debugger and exposes the current yielded subject, with DevTools open. cy.pause() pauses command execution so I can step through queued commands and inspect the application between them. I choose based on whether I need one subject or the broader command sequence.
Q: How do you debug a Cypress timeout?
I identify the condition that never became true and inspect the setup, trigger, network operation, render, and assertion in order. I do not treat elapsed time as the cause. I increase a local timeout only when the condition and its accepted latency are both correct.
Q: How do you approach a test that fails only in CI?
I reproduce the CI command and align browser, viewport, build, environment, data, timezone, and parallel context. I preserve and compare screenshots, video, reporter output, requests, and server logs. Then I test one hypothesis instead of adding retries or sleeps.
Q: Can Cypress retries replace debugging?
No. Retries can preserve additional attempts and reveal intermittency, but a later pass does not remove the race or isolation defect. I compare attempt timelines and fix the first divergent state.
Q: When is a product bug the correct conclusion?
When setup and action are valid, the expected contract is clear, and evidence shows the application or service violated it. For example, a correct approval request returning 500 should not be hidden by weakening the UI assertion. The test has done its job.
Common Mistakes
- Attaching a Node debugger to browser-side spec code and assuming Cypress breakpoints are broken.
- Placing
debuggerbetween queued commands instead of inside an executed callback. - Editing the test before reproducing and recording the original failure signature.
- Reading only the final assertion and ignoring the preceding setup, action, and request timeline.
- Using
cy.wait(milliseconds)to make a race less visible. - Adding
{ force: true }without investigating why the control is not actionable. - Registering an intercept after the request already started.
- Broadly suppressing uncaught application exceptions.
- Replacing a retryable assertion with a one-time assertion in
.then(). - Calling an isolated pass proof when the failure originally required suite order or parallel collision.
- Leaving
.only,cy.pause(),.debug(), verbose logs, or temporary credentials in committed code. - Changing the test when evidence points to a product or environment defect.
Conclusion
The reliable answer to cypress how to debug a failing test in VS Code is a two-runtime workflow. Use VS Code to open the exact failing line, understand types, edit the smallest cause, and launch focused reruns. Use the Cypress Command Log and browser DevTools to inspect spec execution, and reserve Node debugging for configuration and task handlers.
Start with one current failure. Classify its boundary, add the narrowest pause or intercept that exposes evidence, and prove one causal fix in isolation and in the original suite context. That process produces stable tests and better defect reports, not merely greener reruns.
Interview Questions and Answers
Explain your workflow for debugging a failed Cypress test.
I reproduce the smallest failure with matching inputs, then read the error and Command Log as a timeline. I classify the broken boundary as setup, action, request, render, or assertion and add the narrowest useful pause or intercept. After one causal change, I rerun the focused test, full spec, and original CI context.
Why can a VS Code Node breakpoint miss Cypress spec code?
The spec is bundled and executed in the browser, not in the Node process targeted by that debugger. I use browser DevTools for spec callbacks. I use the VS Code Node debugger for configuration and task code that actually runs in Node.
When would you use `.debug()` instead of `cy.pause()`?
I use `.debug()` when I need the exact subject yielded by a query or command. I use `cy.pause()` when I want to step through several commands and inspect application state between them. Both are temporary diagnostic tools.
How do you distinguish a synchronization problem from a product defect?
I verify setup and the user action, then observe the operation that should change state. If the correct request succeeds and the UI never reflects its valid response, the issue may be rendering or state management. If the service returns an invalid response, I preserve the test and report the product failure.
What is wrong with adding `cy.wait(5000)` to a flaky test?
It waits regardless of application speed and does not prove the required event occurred. It can hide a race while making the suite slower. I replace it with an observable request, DOM, navigation, or clock condition.
How do you debug detached DOM element errors?
I identify the action that caused a rerender, inspect whether the old element is still connected, and query again after that boundary. I avoid retaining jQuery elements across application state changes. The test should act once, then obtain a fresh subject for the result.
How do you prove a Cypress fix did not create a false positive?
I remove diagnostic markers, run the test repeatedly and in its suite context, and inspect the final diff for causal alignment. I also deliberately break the expected outcome once and confirm the test fails for the intended reason. Then I restore it and run normal verification.
Frequently Asked Questions
Can I debug a Cypress test directly in VS Code?
Use VS Code to launch, navigate, and edit the test, but debug normal Cypress spec execution in the launched browser's DevTools. Cypress spec code runs in the browser, while VS Code's Node debugger is suited to Node-side configuration and task handlers.
Why is my Cypress breakpoint not working in VS Code?
The breakpoint is often attached to a Node process while the spec executes in a browser. Open browser DevTools and use `.debug()`, `cy.pause()`, or a `debugger` inside an executed Cypress callback.
How do I pause a Cypress test on a failing command?
Place `cy.pause()` before the suspected command to step through the queue. To inspect a specific yielded subject, chain `.debug()` after its query and keep browser DevTools open.
Should I add a fixed wait when a Cypress test fails intermittently?
Usually no. Wait on an observable condition such as an aliased request, route, visible state, enabled control, or deterministic timer instead of elapsed time.
How do I debug a Cypress test that fails only in CI?
Match the CI browser, command, build, viewport, data, environment variables, timezone, and parallel context. Compare original screenshots, video, requests, reporter output, and application logs before changing the test.
Does `.debug()` change the Cypress subject?
No. `.debug()` yields the same subject it received, so you can continue the chain. It requires browser DevTools to be open for the breakpoint to activate.
Can I debug `cy.task()` code with VS Code?
Yes. `cy.task()` handlers and `setupNodeEvents` execute in Node, so a Node debugging session can inspect them. Keep filesystem and database diagnosis separate from browser-side DOM debugging.