skip to content

Why does a console.log() in a Cypress spec never reach the terminal running cypress run?

level: juniorimportance: must knowfreq 58%

answer

  1. Two processes, one run
  2. Which half evaluates the spec?
  3. The terminal belongs to Node
  4. Headless browsers still have a console
  5. cy.task carries a value back to Node

basics

~20 s

Spec code is evaluated inside the browser, so console.log() writes to that browser's DevTools console. The terminal belongs to Cypress's separate Node process, which only prints what code in cypress.config.js and its Node handlers log.

solid answer

~40 s

A Cypress project runs as **two processes**. Your specs, your support file and every `cy` command are bundled and evaluated *inside the browser*, in the same event loop as the application under test, so a plain `console.log()` there lands in that browser's DevTools console — never in your shell. The terminal belongs to the other half: the Cypress Node process, which loads `cypress.config.js`, runs `setupNodeEvents`, drives the browser and prints the run summary. In `cypress run` the browser is usually headless, so spec output has nowhere visible to go at all. To get a value from the spec into the terminal you have to cross the boundary deliberately — `cy.task('log', value)` hands it to a Node handler, and that handler's `console.log()` is the one you see.

code

javascript · 11 lines
javascript
// cypress/e2e/statements.cy.js - bundled in Node, evaluated in the browser
describe('statement list', () => {
  it('shows the account statements', () => {
    cy.visit('/statements')

    cy.get('[data-cy=statement-row]').then(($rows) => {
      console.log('rows on page:', $rows.length) // browser DevTools console
      cy.task('log', `rows on page: ${$rows.length}`) // terminal, via Node
    })
  })
})

go deeper

for a junior

Be ready to say plainly that spec and support files run in the browser while the config file runs in Node, and to name where each one's output shows up.

for a middle

Explain the boundary itself: what the preprocessor bundles for the browser, what stays in Node, and why crossing it needs an explicit command rather than a plain import.

for a senior

Show how you debug a run where the terminal is all you have. An interviewer expects you to say which diagnostics you route through a Node handler and which you leave in the browser console for open mode.

for a principal

Own the convention for the suite: what a failing pipeline run must print without a local re-run, where run-level diagnostics live, and how much logging the team is allowed to add before it becomes noise.

## Cypress is two processes, not one When you type `cypress open` or `cypress run`, two things start. The **Node process** is what your shell actually launched. It reads `cypress.config.js`, calls `setupNodeEvents`, runs the built-in file server and proxy, launches and controls the browser, writes videos and screenshots, prints the spec summary and sets the exit code. Its standard output *is* your terminal. The **browser half** is a real browser that the Node process drives. Cypress loads a runner page there which holds the application under test and your test code side by side, and evaluates your spec in that page, in the same event loop as the app. That co-location is the whole point of the tool — it is why `cy` commands have native access to the app's DOM, timers and window object with no serialization in between. Your spec is never executed in Node. It is compiled and bundled **in** Node and then **served to** the browser. So when a spec calls `console.log()`, it is calling the browser's console API, and the output goes exactly where any other page's logging goes: the DevTools console of the browser Cypress launched. ## Where each file in a Cypress project runs | File | Runs in | Its `console.log()` shows up in | |---|---|---| | `cypress/e2e/statements.cy.js` (a spec) | Browser | The browser's DevTools console | | `cypress/support/e2e.js` (support file) | Browser | The browser's DevTools console | | A helper module the spec imports | Browser | The browser's DevTools console | | `cypress.config.js`, top level | Node | The terminal | | The `setupNodeEvents(on, config)` body | Node | The terminal | | An `on('task')` handler | Node | The terminal | | The bank statement app and its embedded PDF viewer | Browser | The browser's DevTools console | The split is not about folders — it is about **who evaluates the file**. Everything the preprocessor bundles is browser code; everything the config file pulls in is Node code. ## Why this bites hardest in CI - In `cypress run` the browser is normally **headless**, so there is no DevTools window to read. Nothing is lost — it simply has no visible destination. - Cypress does **not** pipe the browser console into the terminal. A terminal log line you did not put there yourself came from the Node half or from Cypress's own reporter. - Open mode hides the difference, because you can pop DevTools open next to the Command Log and see spec logging immediately. Teams therefore discover the boundary only when the same test is running in a pipeline. - Debugging output added *inside* a `.then()` callback is still browser code. Nesting it more deeply does not move it to Node. ## Crossing the boundary on purpose If a value must be in the terminal — a seeded account number, the statement period a test picked, an id you will need to reproduce a CI failure — route it through the Node half: 1. Register a handler in `setupNodeEvents` under the `task` event that does the printing and returns `null`. 2. Call it from the spec with `cy.task('log', message)`, wherever the value is in scope. 3. Pass only data that survives `JSON.stringify()`; a jQuery object or a DOM node will not make the trip intact. For a bank statement suite this is the difference between a CI log that says only `1 failing` and one that says which account and which statement period the failing run used. ## Three surfaces, not two Open mode actually gives you three places output can appear, and confusing them is the usual follow-up: - **The terminal** — the Node half: config-time logging, task handlers, Cypress's own run reporting. - **The Command Log**, the panel beside your app — one entry per `cy` command, plus anything you write with `cy.log()`. Clicking an entry prints its details to the browser console. - **The browser DevTools console** — raw `console.log()` from your spec, your support file and the application under test itself. Knowing which of the three you are looking at tells you which process produced the line, and that is usually the fastest way to work out where a problem actually lives.

  • If the spec runs in the browser, how do the test results end up in the terminal at all?
    The browser half reports every test event back to the Cypress Node process over the socket connection between them. The Node half owns the reporter, so it is what formats the passing and failing counts, writes screenshots and video, and finally sets the exit code your CI job reads.
  • Does a console.log() in cypress/support/e2e.js behave like one in a spec or one in the config file?
    Like one in a spec. The support file is bundled by the same preprocessor and evaluated in the browser before each spec runs, so its output goes to the browser's DevTools console. Only `cypress.config.js` and the Node handlers it registers write to the terminal.

The spec is a passenger inside the car and the Node process is the mechanic outside with the diagnostic laptop. Talking inside the car does not reach the workshop; you have to key the radio, which is what cy.task does.

saying these in an interview costs you the question

  • Thinks the spec and the config file run in the same process
  • Expects a headless browser's console output in the terminal
  • Says cy.log() prints to the terminal as well
  • Believes running headed moves spec logging into the shell
  • Cannot say which half loads cypress.config.js