In Cypress, what does a .then() callback's return value do to the next command's subject?
answer
- Who decides what the next command sees
- Return a value, replace the subject
- Return nothing, keep the old subject
- Inner cy commands link to the outer chain
- Braces around an arrow body change everything
basics
~20 sA returned value becomes the subject of the next Cypress command. Returning nothing leaves the previous subject in place - unless the callback issued Cypress commands, in which case the last of those commands' yield carries on instead.
solid answer
~40 s`.then(cb)` hands you the current subject and lets you decide what the next command gets. Return a value - a string, a number, an object, a jQuery element - and it **replaces** the subject, so `cy.get('[data-cy=book-row]').first().then(($row) => $row.attr('data-loan-id')).should('match', /^L-\d{4}$/)` asserts on the string, not on the row. Return nothing at all and the previous subject carries through unchanged, which is why you can drop a `.then()` into a chain purely to inspect something and keep going. The case that surprises people is a callback that returns nothing **but issues Cypress commands**: Cypress links that inner chain to the outer one, so the last inner command's yield becomes the outer subject. Returning a Cypress chain explicitly does the same thing deliberately, and a returned promise is awaited before its resolved value is used.
code
javascript · 15 linesit('shows both subject rules in one spec', () => {
cy.visit('/catalogue')
cy.get('[data-cy=book-row]')
.first()
.then(($row) => $row.attr('data-loan-id'))
.should('match', /^L-\d{4}$/)
cy.get('[data-cy=book-row]')
.first()
.then(($row) => {
expect($row.text()).to.contain('Dune')
})
.should('have.attr', 'data-cy', 'book-row')
})go deeper
Know that the .then() callback receives the previous command's subject, and that returning a value from it changes what the next command works on. Practise reading one attribute off a row and asserting on it.
Be ready to state all four cases without hedging, including what happens when the callback issues Cypress commands and returns nothing. Expect to be handed a short chain and asked what the final assertion runs against.
An interviewer at this level expects you to spot the silent subject swap in someone else's spec and to say how you would read the Command Log to confirm it, rather than guessing from the source.
Set the convention that makes this class of bug rare: when a callback runs Cypress commands, the chain either ends there or returns its subject explicitly. Decide how that is enforced in review rather than relearned per incident.
## What `.then()` is for Cypress commands pass a single value along a chain - **the subject**. Most of the time you never touch it: `cy.get('[data-cy=loan-row]').first().should('contain', 'Dune')` moves a jQuery collection from command to command without you naming it once. `.then(cb)` is the step where you *do* get your hands on it. The callback receives the current subject as its first argument, and what the callback gives back decides what the rest of the chain sees. That makes `.then()` two things at once: a way to read a subject, and a way to swap it. Confusing those two roles is where most subject bugs in a Cypress suite start. ## The rules, in order 1. **Return a value and it becomes the subject.** Any value at all - a string, a number, an object, a jQuery element. The next command receives it instead of what came in. 2. **Return nothing and the previous subject carries through.** A callback that ends in an `expect(...)` statement, a `console.log`, or a plain assignment returns `undefined`, and Cypress hands the original subject on unchanged. 3. **Return nothing after issuing Cypress commands and the last one's yield wins.** Cypress links a chain started inside the callback to the outer chain, so `cy.get('[data-cy=due-date]')` inside a `.then()` quietly becomes the outer subject. 4. **Return a Cypress chain and Cypress waits for it.** `return cy.get('[data-cy=due-date]')` is rule 3 made explicit and deliberate. A returned promise is awaited the same way, and its resolved value becomes the subject. ## The table version | the callback does | the next command's subject | |---|---| | `return $row.attr('data-loan-id')` | the attribute string | | `return { id: 'L-1204' }` | that object | | `expect($row).to.be.visible` (no return) | the same `$row` it came in with | | `cy.get('[data-cy=due-date]')` (no return) | the due-date element | | `return cy.get('[data-cy=due-date]')` | the due-date element | ## The case that surprises people Rule 3 is the one that bites in real suites, because nothing in the code says a swap happened. Consider a library catalogue test that wants to click borrow and then assert on the row it started from: ```javascript cy.get('[data-cy=book-row]') .contains('Dune') .then(($row) => { cy.wrap($row).find('[data-cy=borrow]').click() }) .should('have.attr', 'data-cy', 'book-row') ``` The `.should()` does **not** run against `$row`. The callback returned nothing, but it started a Cypress chain, so the outer subject became whatever `.click()` yielded - the borrow button. The assertion fails on an element the author never mentioned, and the error message talks about a button. The fix is to say what you mean: `return cy.wrap($row)` at the end of the callback, or move the assertion into its own chain that queries the row again. ## Why the last inner command wins Rule 3 is not an accident of implementation, it is a deliberate link. While a callback is running, any new chain it starts is recorded as linked to the chain that is waiting on it. When the inner chain finally resolves, its subject is used for the outer one as well - which is what makes a `.then()` whose callback returns `cy.get('[data-cy=due-date]')` behave the way a reader expects. An explicit non-null return breaks that link deliberately, so your value wins over anything the callback happened to queue. That single mechanism is why rules 1, 3 and 4 sit together so awkwardly: they are three faces of the same link, seen from the outside. ## Reading it in practice - **Use a return when you want to transform.** Pulling one field out of a subject and asserting on it is what a returned value is for. - **Use no return when you want to inspect.** Log it, read it into a variable declared outside the callback, assert on it inline - the chain keeps its subject. - **Be explicit whenever the callback runs Cypress commands.** If you also want a specific subject afterwards, return it. Silence is a decision Cypress makes for you. - **Watch what a one-line arrow returns.** `($row) => $row.text()` returns the text; `($row) => { $row.text() }` returns `undefined`. The braces are load-bearing. - **Remember that `.spread()` follows the same rules.** It is `.then()` for an array-like subject, expanded into separate arguments, with identical return-value behaviour. ## Getting it wrong The two failure shapes to recognise in a review are a chain whose assertion suddenly talks about the wrong element - rule 3 firing unnoticed - and a callback whose author added `return` to a debugging `console.log` line, silently setting the subject to `undefined` for everything downstream. Both are invisible in the code and obvious in the Command Log, where each command shows the subject it actually received. When a chain stops making sense, read the log top to bottom and find the step where the subject changed shape.
- In Cypress, how do you keep the original subject after a .then() callback that has to compute a value?Compute it but do not return it - a callback that returns nothing leaves the previous subject in place. If you need the computed value later, capture it in a `const` declared outside the callback and read it in a subsequent `.then()`, or hand the original on explicitly with `return cy.wrap($original)`.
- What does a Cypress .then() callback yield when it explicitly returns cy.get('[data-cy=loan-row]')?The jQuery collection that the inner `cy.get()` yields. Cypress waits for the returned chain to finish and uses its subject as the outer chain's subject, so the next command acts on the loan rows rather than on whatever the outer chain was holding before.
The subject is a baton in a relay. .then() is the one leg where you are allowed to hand the next runner a different baton, and if you hand over nothing at all they simply keep running with the one they already had.
saying these in an interview costs you the question
- Says .then() always yields back the subject it received
- Thinks returning nothing sets the subject to undefined
- Cannot explain why an inner cy.get() changes the outer subject
- Misses that an arrow body in braces returns nothing
- Assumes .spread() and .then() differ in return-value rules