Cypress 16 removed cy.exec() - how do you run a setup command now?
answer
- The shell door closed in version 16
- Setup now runs in Cypress's Node process
- One command replaced by an event name
- cy.task supersedes the removed cy.exec
- execTimeout became taskTimeout, 60000 ms
basics
~20 scy.exec() was removed in Cypress 16 and now throws; use cy.task() instead, calling a handler registered under the task event in setupNodeEvents. The execTimeout option was removed with it, and taskTimeout, default 60000 ms, replaces it.
solid answer
~50 s`cy.exec()` was removed in Cypress 16.0.0 after being deprecated in 15.21.0; calling it throws an error pointing you at `cy.task()`. `cy.exec()` shelled out on the machine running Cypress, so its behaviour depended on the operating system, the shell, the login environment and whether a terminal was attached. `cy.task(event, arg, options)` instead invokes a handler registered under the `task` event in `setupNodeEvents`, running in the Node process that loaded your Cypress config - no shell resolution, no per-platform quoting. It also hands back a real value rather than stdout you have to parse, so `cy.task('seedExpenseReport', { total: 42.5 })` can yield the created report object. The handler must return a value or `null`; `undefined` fails the command. `execTimeout` went with the command - setting it only prints a warning - and `taskTimeout` (60000 ms) bounds `cy.task()`.
code
javascript · 15 lines// Removed in Cypress 16 - this now throws:
// cy.exec('node scripts/seed-expenses.js').its('stdout')
describe('expense report detail', () => {
beforeEach(() => {
cy.task('resetExpenseLedger')
cy.task('seedExpenseReport', { employee: 'jane', total: 42.5 }).then((report) => {
cy.visit(`/reports/${report.id}`)
})
})
it('shows the submitted receipt total', () => {
cy.get('[data-cy="report-total"]').should('have.text', '$42.50')
})
})go deeper
Recall that cy.exec() is gone in Cypress 16 and cy.task() is the replacement. Be ready to say where a task handler is registered and that a task must return a value or null.
Explain why the shell was the problem: OS, shell, login environment and attached terminal all leaked into the test. Contrast parsing stdout with receiving a structured value.
Be ready to describe migrating a real suite off cy.exec() - finding the call sites, deciding which become tasks and which become HTTP calls, and deleting execTimeout from the config.
Own the argument for keeping the Node-side surface small: every task is a permanent extension point your team maintains, so justify each one rather than porting shell scripts across wholesale.
## What was removed, and what happens if you call it `cy.exec()` is gone as of Cypress **16.0.0**. It was deprecated in `15.21.0` and removed in the next major. Calling it in a spec no longer runs anything: the driver throws immediately with *"`cy.exec()` was removed in Cypress version 16.0.0. Please update to use `cy.task()` instead, which runs in Node and does not depend on the operating system, shell, or terminal of the machine running Cypress."* The `execTimeout` configuration option went with it. Cypress prints a warning saying it was removed in 16.0.0 and then ignores the value, so a configuration that still carries `execTimeout: 120000` bounds nothing at all. Delete it and reach for `taskTimeout` instead. This is worth knowing precisely, because a large share of published Cypress material still opens its seeding section with `cy.exec('npm run db:reset')`. On Cypress 16 that line is a hard failure at the moment the command runs, not a deprecation notice you can defer. ## Why the shell door was closed `cy.exec()` ran a command **through the shell on the machine running Cypress**, which made a test's setup step depend on things a test has no business depending on: - the operating system, and therefore which quoting and escaping rules apply to the command string - which shell is configured, and whether its login profile has been sourced - whether a terminal is attached — different on a developer laptop than under a CI runner - `PATH` resolution for the binary named in the string - the output contract: you received `stdout` as text and had to parse a value back out of it `cy.task()` has none of that surface. Its handler runs in the **Node process that loaded your Cypress configuration**, so there is no shell, no per-platform quoting, no `PATH` guessing — and the value the handler returns arrives in the spec as a value, not as text. ## The replacement, end to end 1. Register a handler for the `task` event inside `setupNodeEvents`, keyed by an event name of your choosing (`'seedExpenseReport'`, `'resetExpenseLedger'`). 2. Call `cy.task(event)`, `cy.task(event, arg)` or `cy.task(event, arg, options)` from the spec. 3. Consume the yielded value with `.then()`, `.its()` or a chained assertion. From the spec's side, the only contract is the event name and whatever comes back; the handler itself lives with the rest of your Cypress configuration. | Cypress 15 and earlier | Cypress 16 | |---|---| | `cy.exec('node seed-expenses.js')` | `cy.task('seedExpenseReport', payload)` | | yields `{ stdout, stderr, exitCode }` | yields whatever the handler returned | | `execTimeout` bounds it | `taskTimeout` bounds it, default `60000` ms | | runs in a shell on the host machine | runs in Cypress's own Node process | | output is text the spec must parse | value arrives already structured | ## The contract `cy.task()` imposes - **The handler must return something.** Returning `undefined` fails the command with *"The task 'X' returned undefined. You must return a value, null, or a promise that resolves to a value or null to indicate that the task was handled."* Return `null` explicitly when there is nothing to hand back. - **An unregistered event name fails too**, and the error lists every task name that *is* registered, which makes a typo obvious rather than mysterious. - **Exactly one argument crosses the boundary.** `cy.task(name, arg, options)` has room for a single `arg`; pass an object and destructure it on the Node side when you need several values. - **The argument is serialized with `JSON.stringify()`.** Functions, regular expressions and symbols are omitted to `null`, and a `Date` arrives on the other side as a string. ## What if the thing you need to run is not Node? Spawn it from inside the handler. Node's `child_process.execFileSync` with an argument array — `execFileSync('docker', ['exec', 'db', 'reset-script'])` — runs the binary directly and skips the shell, which is exactly the property whose absence made `cy.exec()` unreliable. The difference is that the spawning is now explicit, lives in one place instead of scattered through specs, and the task can hand the spec a real value rather than a blob of stdout. ## What this looks like in an expense-report suite A suite that used to shell out to reset the ledger and then scrape a report id out of stdout now calls `cy.task('resetExpenseLedger')` followed by `cy.task('seedExpenseReport', { employee: 'jane', total: 42.5 })`, and the second call yields the created report object. The spec goes straight to `cy.visit('/reports/' + report.id)` with no string parsing, no `.trim()`, and no guessing about which shell CI happened to give it.
- In Cypress 16, what happens if you leave execTimeout in your configuration file?It is ignored. Cypress prints a warning saying `execTimeout` was removed in 16.0.0 and points you at `cy.task()` and `taskTimeout`, then carries on with the option having no effect at all. Delete it, and set `taskTimeout` if the default 60 seconds is not enough for your seeding work.
- Your setup needs a database CLI that is not a Node module. How, without cy.exec()?Spawn it from inside the task handler. Node's `child_process.execFileSync` with an argument array runs the binary directly and avoids the shell, so quoting and `PATH` behave the same on every platform. The handler returns whatever the spec needs - the output, or `null` - and the spec still just calls `cy.task('resetExpenseLedger')`.
saying these in an interview costs you the question
- Claims cy.exec() still works if you avoid CI
- Parses stdout from a task instead of returning a structured value
- Thinks cy.task() runs in the browser beside the test code
- Keeps execTimeout in the config expecting it to bound tasks
- Returns nothing from the handler and calls the failure a Cypress bug