In Cypress, what changes when an `expect()` moves from `.then()` into `.should()`?
answer
- One runs once, one runs again
- should is an assertion; then is not
- The retry re-runs the queries above it
- No cy commands inside a should callback
- A should callback's return value is ignored
basics
~20 sNothing about the code changes; the timing does. A .then() callback runs once against a frozen chain, while a .should() callback is retried, queries and all, until nothing inside it throws or the command timeout expires.
solid answer
~40 s`.then(cb)` is an escape into JavaScript: Cypress waits for the upstream subject, calls the callback **once**, and freezes the chain above it, so nothing re-runs. `.should(cb)` is an assertion. Cypress re-runs the preceding queries and the whole callback against a freshly queried subject until no `expect()` inside throws or `defaultCommandTimeout` (4000 ms by default) expires. The same parse-and-compare block that fails instantly in `.then()` therefore waits out a slow-arriving booking total in `.should()`. The retry is not free: the callback must be side-effect free, and as of Cypress 16 invoking a `cy` command inside it throws, because retrying would enqueue that command repeatedly. A `.should()` callback's return value is also ignored — the original subject flows on — whereas returning from `.then()` replaces the subject.
go deeper
Learn the one-line difference first: the then callback fires once, the should callback fires again and again until it passes. Everything else follows from that.
An interviewer expects the mechanics: which part of the chain re-runs, that the callback must be free of side effects, and that a Cypress command inside it is an error.
Be ready to diagnose the symptom rather than the code — a value check that passes locally and fails under CI load is usually an expect in the wrong callback, not a timeout that is too short.
Set the default. Decide that computed-value checks are written as retrying assertions, and that a bare expect inside then needs a stated reason in review.
## Two callbacks that look identical `.then(($total) => { ... })` and `.should(($total) => { ... })` take the same argument, receive the same subject and can hold exactly the same `expect()` calls. They are not interchangeable. `.then()` is a general-purpose escape into JavaScript; `.should()` is an **assertion**, and Cypress treats the function you hand it as the body of an assertion it is entitled to retry. ## What each one does with time `.then(cb)` waits for the upstream subject to resolve — up to `defaultCommandTimeout`, 4000 ms by default — and then invokes the callback **once**. The Cypress documentation is blunt about it: the callback of `.then()` is not retried, and assertions chained inside it run once and either pass or fail. `.then()` also ends the retryable part of the chain, so nothing above it re-runs; a query that fed it is frozen at the value it produced. `.should(cb)` is a retry boundary. Cypress runs the preceding **query** chain, calls the callback, and if anything inside throws it discards that attempt and starts over — re-running the queries and invoking the callback with a freshly queried subject — until nothing throws or `defaultCommandTimeout` expires. That is the entire difference, and it decides every case where the value under test is still arriving. The canonical shape is a booking total that renders as a placeholder and is filled in when a pricing call returns: ```javascript // one shot: parses the placeholder, fails on the spot cy.get('[data-cy=booking-total]').then(($t) => { expect(Number($t.text().replace('$', ''))).to.eq(420) }) // retried: re-queries, re-parses and re-asserts until the price lands cy.get('[data-cy=booking-total]').should(($t) => { expect(Number($t.text().replace('$', ''))).to.eq(420) }) ``` ## The rules `.should()` imposes in exchange | | `.then(cb)` | `.should(cb)` | |---|---|---| | runs the callback | once | once per attempt, until it passes or times out | | upstream queries | frozen, nothing re-runs | re-run on every attempt | | a `cy` command inside | allowed | throws | | the return value | becomes the next subject | ignored; original subject flows on | | side effects in the body | happen once | happen once per attempt | Two of those rows bite in practice. - **No Cypress command inside the callback.** As of Cypress 16 this is an error, not a warning. The message says `.should()` failed because you invoked a command inside the callback, that `.should()` retries the inner function, and that this would result in commands being added to the queue multiple times — then tells you to use `.then()` instead or move the command outside the callback. - **The return value is ignored.** `.then()` can rewrite the subject by returning a value; a `.should()` callback cannot. Whatever it returns, the next command receives the original subject. That is deliberate: it makes `.should(cb)` safe to drop into the middle of a chain, so `cy.get('[data-cy=room-card]').should(cb).first().click()` still clicks the room cards. ## Choosing between them 1. **Use `.should(cb)`** when every line is a computation over the subject plus assertions on the result — parsing a rate, deriving a total, checking sort order, asserting several properties of one room card together. This is the default for value checks and it costs nothing to prefer. 2. **Use `.then(cb)`** when the block must happen exactly once: queueing further Cypress commands, capturing a value for a later step, calling `cy.task()` to seed or clean up, branching on what the page actually rendered. 3. **Combine them** when the block is genuinely one-shot but the page is still settling — put a retrying assertion immediately in front so `.then()` opens on settled DOM: ```javascript cy.get('[data-cy=booking-total]') .should('not.contain', '—') .then(($t) => { // the assertion above pinned the page; this block runs once, on settled state }) ``` ## Why teams get this wrong The failure mode is quiet. A `.then()` block full of `expect()` calls passes on a fast local machine, where the value is already present when the callback fires, and fails on a loaded CI worker, where it is not. Nothing in the code announces "this check does not wait", so the test looks correct and gets re-run, quarantined, or wrapped in a longer timeout instead of moved. The timeout is the tell: `defaultCommandTimeout` governs how long Cypress waits for a subject to appear, not how many times a non-retrying callback fires, so raising it changes nothing about a one-shot `expect()`.
- What does Cypress yield to the command after a `.should(cb)`?The original subject. A `.should()` callback's return value is ignored, so `cy.get('[data-cy=room-card]').should(cb).first().click()` still clicks a room card. That is deliberate — it lets you drop a retrying group of assertions into the middle of a chain without breaking what follows. `.then()` behaves the opposite way: returning a value from it replaces the subject.
- Why does Cypress throw when a `.should()` callback calls a Cypress command?Because the callback is retried. Each attempt would enqueue the command again, so a callback that retried five times would leave five copies in the command queue in unpredictable order. Cypress fails fast with a message naming the command it caught and pointing you at `.then()`, or at moving the command outside the callback entirely.
saying these in an interview costs you the question
- Says .then() retries the same way .should() does
- Raises defaultCommandTimeout to fix a one-shot check
- Queues cy commands inside a .should() callback
- Thinks a .should() callback's return value changes the subject
- Believes .then() re-runs the queries above it