skip to content

What exit code does `cypress run` return, and what does `--posix-exit-codes` change?

level: middleimportance: must knowfreq 62%

answer

  1. Mocha's convention, not POSIX
  2. A count, not a verdict
  3. An exit status is one byte
  4. One flag collapses it to a boolean
  5. A reserved code for Cloud network failure

basics

~20 s

By default cypress run exits with the number of failed tests, Mocha-style: three failures give status 3, and 0 means everything passed. Passing --posix-exit-codes makes any test failure exit 1 instead, and adds 112 for a Cypress Cloud network failure.

solid answer

~40 s

The default is a tally rather than a verdict: `cypress run` exits `0` when everything passes and with the *number of failed tests* otherwise, so three failures produce status `3`. That is Mocha's convention, not POSIX, and it has two sharp edges — a process status is one byte, so a run with exactly 256 failures gets truncated to `0` by most shells, and `1` is ambiguous between "one test failed" and "Cypress could not run". `--posix-exit-codes` switches to POSIX semantics: `0` for a clean run, `1` for any test failure whatever the count, `1` for a failure to start, and `112` when Cypress could not run because a required connection to Cypress Cloud failed. With the flag on, read the failure count from `cypress.run()` or a structured reporter instead of `$?`.

code

bash · 8 lines
bash
# default: the status is the number of failed tests
npx cypress run --spec "packages/checkout/cypress/e2e/**/*.cy.js"
echo "failed tests: $?"

# POSIX: 1 for any test failure, whatever the count
npx cypress run --posix-exit-codes \
  --spec "packages/checkout/cypress/e2e/**/*.cy.js"
echo "status: $?"

go deeper

for a junior

Remember the headline: 0 means everything passed, and a non-zero status means something went wrong. Knowing that the number is a count of failures, not a generic error, is enough at this level.

for a middle

Be ready to explain both behaviours and why two exist — the Mocha-style tally versus POSIX — and to name the truncation and ambiguity problems the flag exists to solve.

for a senior

Talk about operating this: which behaviour you standardise on for a shared pipeline, how you distinguish a network failure from a red suite, and where the failure count is read from once the status stops carrying it.

for a principal

Own the contract between the suite and everything that calls it, and decide whether a count in the status is information worth keeping or an inconsistency to remove across every team's pipelines.

## The default: the exit code is a tally, not a verdict `cypress run` follows Mocha's convention rather than the POSIX one. When every test passes it exits `0`. When *n* tests fail it exits with *n* — three failing tests produce exit status `3`, not `1`. That is deliberate: the status carries a count, so a caller that only checks `status != 0` still works, while a human reading the terminal learns how bad it was. ## Where the tally breaks down - **Eight bits.** A process exit status is one byte. Most shells apply modulo 256 to anything larger than 255, so a run with exactly 256 failing tests reports `0` — a false negative that lets a completely broken suite pass a stage. - **Reserved values.** Because certain conditions return their own reserved codes, a non-zero status does not by itself tell you whether tests failed or Cypress never got started. - **Ambiguity at `1`.** Under the default behaviour, "one test failed" and "Cypress could not run at all" are both `1`. ## `--posix-exit-codes` Passing `--posix-exit-codes` swaps in POSIX-compliant behaviour: any test failure exits `1`, however many tests failed, and additional conditions become distinguishable that the default cannot express. | Exit condition | default | with `--posix-exit-codes` | |---|---|---| | All tests pass | `0` | `0` | | *n* tests fail | *n* | `1` | | Cypress could not run because a connection to Cypress Cloud was required and the network failed | `1` | `112` | | Cypress could not run for any other reason, including no spec files found | `1` | `1` | The `112` row is the one that earns the flag on a recorded or parallelised run: it separates "the suite is red" from "we never reached Cypress Cloud", which are different incidents with different owners. ## Getting the count back Once `--posix-exit-codes` collapses every failure to `1`, the failure count has to come from somewhere else: - `cypress.run()` from the Node module resolves with `totalFailed`, `totalPassed` and the per-spec `stats` objects. - Structured reporter output — `--reporter junit`, or a JSON reporter — carries the same numbers in a file the pipeline can parse. Either way, treat `$?` as a boolean once the flag is on. ## What changed in Cypress 16 Cypress 16 fixed a case where a run in which every test passed could still exit `1`, making a green run indistinguishable from a single failure. Cleanup that fails or takes too long while Cypress shuts down no longer changes the exit code; it is reported in the run output instead. If you carry a "sometimes the passing run goes red" workaround from an older version, delete it. ## Choosing between the two behaviours 1. Leave the default in place when one `cypress run` owns the stage and nothing parses the number — the count is free information in the terminal. 2. Add `--posix-exit-codes` when a wrapper, a shell `set -e`, or a job runner treats the status as a boolean, when a suite is large enough that a multiple of 256 is conceivable, or when you need `112` to tell a Cloud network failure apart from test failures. 3. Either way, read counts from the reporter output or the Module API, never from the exit status alone. One more flag interacts with all of this: by default a run that finds no spec files exits `1`, which is usually what you want. `--pass-with-no-tests` makes that case exit `0` instead, for pipelines where a spec set is conditionally generated and legitimately empty. ## Losing the status on the way out Whichever behaviour you choose, the status still has to survive the shell that ran the command, and that is where it is most often dropped: - **Piping.** In a POSIX shell, `cypress run | tee run.log` reports `tee`'s status, not Cypress's. The shell's `PIPESTATUS`/`pipefail` mechanisms exist for exactly this, and forgetting them turns every red run green. - **Reading `$?` late.** `$?` holds the status of the *last* command. An `echo` between the run and the check overwrites it; capture it into a variable on the very next line. - **Wrapping in another script.** A wrapper that runs Cypress and then does cleanup exits with the cleanup's status unless it saves and re-exits with Cypress's. None of these are Cypress behaviours, but every one of them produces the same symptom — a suite that fails loudly in its own output while the stage that ran it reports success — and it is worth ruling them out before blaming the exit-code contract itself.

  • With `--posix-exit-codes` on, how do you still report how many Cypress tests failed?
    Take it from a structured source rather than the status. `cypress.run()` resolves with `totalFailed`, `totalPassed` and per-spec `stats`; a reporter such as `--reporter junit` writes the same counts to a file the pipeline can parse. The exit status becomes a boolean and nothing more.
  • What does `cypress run` exit with when `--spec` matches no spec files?
    `1` — Cypress could not run, which is the same status it uses for other start-up failures, with or without `--posix-exit-codes`. If an empty spec set is legitimate in your pipeline, `--pass-with-no-tests` makes that case exit `0` instead.

The default status is a scoreboard, not a whistle: it tells you how many tests lost, where POSIX only wants to know whether anybody did.

saying these in an interview costs you the question

  • Says cypress run always exits 1 when any test fails
  • Assumes a non-zero exit always means tests failed
  • Never considers that shells truncate a status above 255
  • Parses the exit status to report a failure count under --posix-exit-codes
  • Thinks --posix-exit-codes changes which tests run