In Cypress, what happens when two files add a custom command with the same name?
answer
- Two guards, and only two
- Built-in names are rejected loudly
- Your own names are not checked
- The registry is keyed by name
- Import order picks the winner
basics
~20 sNothing is reported. Cypress.Commands.add() only rejects a name belonging to a built-in command or reserved internally by Cypress; a second registration of one of your own names silently replaces the first, and whichever file loads last wins.
solid answer
~40 s`Cypress.Commands.add()` guards two things and no more. If the name matches a **built-in** Cypress command it throws — *`Cypress.Commands.add()` is used to create new commands, but `visit` is an existing Cypress command* — and points you at `Cypress.Commands.overwrite()`. If the name is reserved internally by the runner it throws a different error. A collision between two of *your* commands is not checked at all: registration is a write into a registry keyed by name, so the second `add('seedReport', ...)` replaces the first with no warning. Because the support file and everything it imports are evaluated before every spec, import order decides which body runs, and a merge that pulls in a second commands file can silently change what `cy.seedReport()` does across the whole suite. Grep the name before you add it; no tooling will.
code
javascript · 18 lines// cypress/support/report-commands.js
Cypress.Commands.add('seedReport', (status) => {
cy.request('POST', '/api/expense-reports', { status, total: 412.9 })
})
// cypress/support/legacy-report-commands.js
Cypress.Commands.add('seedReport', (status) => {
cy.visit('/reports/new')
cy.get('[data-cy=report-status]').select(status)
cy.get('[data-cy=save-report]').click()
})
// cypress/support/e2e.js
import './report-commands'
import './legacy-report-commands'
// cy.seedReport('approved') now runs the second body, in every spec.
// Cypress reported nothing.go deeper
Know that a custom command name is global to the whole run, and that you register it in the support file or a file the support file imports. That is enough to see why two files can clash.
Explain the two guards Cypress applies and the one case it does not: built-in and reserved names throw, your own names are silently replaced by the last registration evaluated.
Talk about diagnosis. Describe how a suddenly slow or newly failing spec traced back to a second registration, and how you confirmed it by walking the support file's imports.
Own the convention. Decide whether commands come from one registration file, whether a shared package may add names, and what makes adding a name to the global chain a reviewable change.
## Two guards, and only two `Cypress.Commands.add(name, options, fn)` checks the name it is given against exactly two lists before it registers anything. 1. **Built-in command names.** Cypress records the names of its own commands as it installs them. Passing one back to `add()` throws: *"`Cypress.Commands.add()` is used to create new commands, but `visit` is an existing Cypress command. Please use `Cypress.Commands.overwrite()` if you would like to overwrite an existing command."* 2. **Names reserved internally.** The runner keeps its own properties on the `cy` object — machinery, not commands — and a name that collides with one throws a different error: *"cannot create a new command named `state` because that name is reserved internally by Cypress."* `cy.mount()` and `cy.hover()` are deliberate exceptions. Cypress ships them as placeholders precisely so that you can register your own without tripping the first guard. ## The case nobody guards Neither guard covers a collision between two commands **you** wrote. Registration is a write into a registry keyed by name, followed by installing the function on the chain. A second write to the same key replaces the first. There is no duplicate check, no warning in the Command Log, and no line in the run output. So all of these are silent: - Two support files each add `seedReport`. - A shared internal package of commands and a project-local `commands.js` both add `signInAs`. - A merge lands a colleague's `attachReceipt` next to the one you already had. - A copy-pasted example from a blog post registers `login` on top of yours. In every case `cy.seedReport()` runs one body and behaves as though the other was never written. ## Why load order decides the winner The support file is Cypress's hook into every spec: it is evaluated **before** each spec file in the run, and everything it imports is evaluated with it, top to bottom. The winner is therefore just the last `Cypress.Commands.add()` for that name to execute during that evaluation — decided by the import order in `cypress/support/e2e.js`, or by the order a bundler resolved two packages. Reorder two imports and the whole suite changes behaviour with no diff in any spec. That also means the collision is not detectable from the spec. Nothing about `cy.seedReport()` at the call site says which of two bodies it will run. ## What throws and what does not | You register | Result | |---|---| | `visit` — an existing built-in command | throws, and points you at `Cypress.Commands.overwrite()` | | `state` — an internal `cy` property | throws, name reserved internally by Cypress | | `mount` or `hover` | allowed — Cypress ships these as placeholder names | | `seedReport` you already registered | **silently replaced, last one wins** | | `seedReport` as a query via `Cypress.Commands.addQuery()` | throws, the name is already taken | The asymmetry is the whole point: Cypress protects **its** namespace carefully and leaves yours completely open. ## How the collision presents The symptom is rarely "the command is wrong". It is a spec that suddenly takes eight seconds instead of one, because the surviving `seedReport` drives the UI where the other one called `cy.request()`. Or it is a spec failing on an argument the surviving body does not accept. Because Cypress writes no Command Log entry for a custom command, the log shows the *other* body's commands running and gives you no hint that a second definition exists. The fastest confirmation is to open the support file, follow every import, and count the registrations of that name. ## Keeping the namespace habitable The global chain is a shared resource with no owner, so the discipline has to be social rather than enforced: - **Register in one place.** One file that makes all the `Cypress.Commands.add()` calls, imported once from the support file, so a collision shows up in a single diff. - **Prefix names that belong to your product.** `cy.expSeedReport()` will not collide with a plugin's `cy.seedReport()`. - **Grep before you add.** Searching the repo for the name you are about to register costs a second and is the only check that exists. - **Keep the command count small.** Most collisions happen because a suite has ninety commands and nobody can hold the list in their head. A helper that one spec imports cannot collide with anything. - **Treat a shared command package as a versioned dependency.** Adding a name to it is a breaking change for every consumer that already uses that name for something else. - **Prefer failing loudly.** If you own the registration file, a two-line wrapper around `Cypress.Commands.add()` that throws on a name it has already seen gives you the guard Cypress does not.
- Why does Cypress throw for a built-in name but stay silent for one of yours?It tracks its own command names as it installs them and refuses to let you shadow them accidentally, steering you to `Cypress.Commands.overwrite()` instead. Names you add are never recorded on that list, so the registry simply accepts the newer entry. `mount` and `hover` are exempt placeholders you are expected to define yourself.
- How would you detect a duplicate Cypress command registration before it bites?Register everything from one file and grep that file for the name first. If you want a real guard, wrap `Cypress.Commands.add()` in a helper that keeps a set of names it has seen and throws on a repeat. In TypeScript, duplicate entries in the `Chainable` interface will also surface as a conflicting declaration.
saying these in an interview costs you the question
- Assumes Cypress errors on any duplicate command name
- Thinks Cypress.Commands.add() logs a duplicate-name warning
- Believes both bodies run, in registration order
- Cannot explain why the support file makes a name global