skip to content

In a Cypress test, why does .debug() pause where a bare debugger line does not?

level: middleimportance: must knowfreq 60%

answer

  1. The body runs before the commands do
  2. The breakpoint needs to be queued too
  3. Wrap it in a callback Cypress invokes
  4. A shorthand command does this for you
  5. DevTools must already be open

basics

~20 s

A bare debugger in the test body runs immediately, before any queued cy command has executed, so it stops on an untouched page. .debug() is itself queued, so it fires in order, with the previous command's subject in hand — the same effect as a debugger inside .then().

solid answer

~50 s

Cypress commands enqueue work rather than doing it; the `it()` body runs straight through and Cypress drains the queue afterwards. So a `debugger` written as the last line of the body executes *first*, before `cy.visit()` has loaded anything — you stop on a blank page and conclude the test is broken. Two things fix that. Put the `debugger` inside a `.then()` callback, which Cypress invokes at that point in the queue with the yielded subject as the argument. Or chain `.debug()`, which is a queued command that does the same thing for you: it prints the previous command's name, its arguments and the current subject to the console, exposes that subject as `subject`, and then hits `debugger`. Both need DevTools open — a `debugger` statement with no debugger attached is a no-op. `.debug()` yields its subject unchanged and is safe to chain further commands from.

code

javascript · 14 lines
javascript
it('shows the oldest open ticket first', () => {
  cy.visit('/queue')

  // WRONG: runs before cy.visit() has loaded anything
  // debugger

  // RIGHT: queued, so it fires with the rows in hand
  cy.get('[data-cy=ticket-row]').then(($rows) => {
    debugger
  })

  // RIGHT: same moment, and logs name, args and subject
  cy.get('[data-cy=ticket-row]').first().debug().should('contain', 'OPEN')
})

go deeper

for a junior

Recall that a plain debugger line in the test body fires before the commands run, and that putting it inside a .then() callback or chaining .debug() fixes it.

for a middle

Explain the enqueue-then-drain model that causes it, and name what .debug() prints: the previous command's name, its arguments, and the current subject exposed as the subject variable.

for a senior

Be ready to say when you reach for .debug() rather than cy.pause() on a suite that misbehaves only sometimes, and why DevTools being open is a precondition rather than a nicety.

for a principal

Take a position on breakpoints in committed specs: they are an interactive tool, and a suite whose failures can only be understood by attaching a debugger is one whose failure output is doing too little.

## Why the bare `debugger` fires too early Your Cypress spec body is ordinary synchronous JavaScript. When you write this: ```js it('assigns the oldest open ticket', () => { cy.visit('/queue') cy.get('[data-cy=ticket-row]') debugger // does not do what you want }) ``` `cy.visit()` and `cy.get()` return immediately, having *enqueued* work for Cypress to do after the body returns. The `debugger` on line three is the only line that actually executes anything, and it executes before the browser has visited `/queue`. You stop in DevTools looking at a blank runner, with none of your commands run yet. That is not a Cypress quirk you can configure away — it is the direct consequence of commands being queued rather than executed inline. The fix is to move the breakpoint into something Cypress calls *during* the drain. ## The two ways to land the breakpoint at the right moment 1. **`debugger` inside a `.then()` callback.** `.then()` takes a function and Cypress invokes it at that command's turn, passing whatever the previous command yielded. The `debugger` inside it therefore fires with the page loaded and the subject in the callback's parameter. 2. **Chain `.debug()`.** This is the shorthand: a queued command whose whole job is to log the previous command's details and hit `debugger` at the right point in the sequence. ```js cy.get('[data-cy=ticket-row]').then(($rows) => { debugger // $rows is the real jQuery collection }) cy.get('[data-cy=ticket-row]').debug() // same moment, less typing ``` ## What `.debug()` prints, and the `subject` variable When `.debug()` runs it writes a small block to the browser console before stopping: - **Command Name** — the name of the previous command, so you know which link in the chain you are standing on. - **Command Args** — the arguments that command was called with, which is where a wrong selector usually announces itself. - **Current Subject** — the value being yielded down the chain. It then exposes that subject as a variable named `subject` in the paused scope, so at the DevTools console you can evaluate `subject.length`, `subject.text()` or `subject[0].outerHTML` without hunting through the call stack for it. That is the practical difference from a hand-written `debugger`: you get the identifying information laid out for you rather than reconstructing it from frames. ## Behaviour worth knowing | | bare `debugger` in the body | `debugger` inside `.then()` | `.debug()` | |---|---|---|---| | When it runs | before any command | at that point in the chain | at that point in the chain | | Subject available | none | the callback parameter | the `subject` variable | | Prints command name and args | no | no | yes | | Needs DevTools open | yes | yes | yes | | Safe to chain after | n/a | yes | yes | Other details that come up: - **DevTools must be open.** A `debugger` statement with no debugger attached is a no-op the engine steps straight past, so `.debug()` looks like it did nothing. Open DevTools *before* the run reaches it. - **`.debug()` is a query in Cypress 16**, yields the same subject it was given, and is safe to chain further commands from — unlike `cy.pause()`, after which a DOM subject may have gone stale while you poked at the page. - **It runs assertions not at all.** Like `cy.pause()`, an assertion chained past `.debug()` passes through as if the command were absent. - **It takes one option, `log`**, defaulting to `true`, which controls whether the `debug` entry appears in the Command Log. - **It stops once.** The breakpoint fires the first time that command executes, not on every retry of a surrounding chain. ## Choosing between them on a flaky suite On a support-ticket queue that only misbehaves sometimes, the two tools answer different questions. `.debug()` answers *"what exactly did this command yield?"* — you stop on one link, read the subject, and see whether the selector matched the row you meant. `cy.pause()` answers *"at which step did the page stop matching my model of it?"* — you step forward one command at a time watching the app preview. A common working order is: 1. Reproduce in `cypress open`. 2. `cy.pause()` before the suspicious region and step until the app diverges from what you expected. 3. Chain `.debug()` onto the one command that yielded something surprising, and read the subject in the console. Reach for the hand-written `debugger` inside `.then()` when you need the breakpoint somewhere `.debug()` cannot go — inside a conditional, after some computation on the subject, or in a helper that is not a Cypress command at all.

  • Why does a Cypress .debug() sometimes appear to do nothing at all?
    Almost always because DevTools were not open when the command ran. A `debugger` statement with no debugger attached is a no-op, so the engine steps straight past it and the run continues to the end. The console block `.debug()` prints is still there afterwards, which is a quick way to confirm the command did execute. Open DevTools before starting the spec, not after it fails.
  • After a Cypress .debug(), is it safe to keep chaining commands?
    Yes. `.debug()` is a query, yields the same subject it was given, and runs no assertions, so `cy.get(sel).debug().should('be.visible')` behaves exactly as it would without the `.debug()`. That is different from `cy.pause()`, after which a DOM subject can go stale because you have been interacting with the page by hand while the run was stopped.

saying these in an interview costs you the question

  • Thinks a debugger in the test body stops after cy.visit()
  • Says .debug() works with DevTools closed
  • Believes .debug() replaces the subject with a log object
  • Cannot name .then() as the place to put a debugger