Why does a Cypress `.then()` that queues a command and returns a value throw?
answer
- Two things are watched inside the callback
- A value computed before the queued work runs
- Returning nothing is always safe
- The message names mixing sync and async
- The fix is to keep chaining
basics
~20 sBecause Cypress reads it as mixing async and sync code: the callback queued commands that have not run yet, then handed back a finished value. Cypress throws with the message that cy.then() failed because you are mixing up async and sync code.
solid answer
~40 sInside a `.then()` callback Cypress watches for two things at once: whether the callback **enqueued at least one cy command**, and what it **returned**. If it queued a command and then returned a truthy value that is not itself thenable, Cypress throws *"`cy.then()` failed because you are mixing up async and sync code"* and tells you that you likely forgot to chain with another `.then()`. The reasoning is that the returned value was computed before the queued commands ran, so it cannot describe the state those commands produce. Returning nothing is always safe, and a callback that returns a value **without** queuing anything is fine too — the check only fires when both happen together. The fix is to keep going down the chain rather than returning early.
code
javascript · 18 lines// Throws: 'cy.then() failed because you are mixing up async and sync code.'
it('borrows a copy and reports the remainder', () => {
cy.visit('/catalogue/dune')
cy.get('[data-cy=book-row]').then(($row) => {
cy.get('[data-cy=borrow]').click()
return Number($row.find('[data-cy=copies]').text())
})
})
// Works: the read happens after the click, as a retried assertion
it('borrows a copy and reports the remainder', () => {
cy.visit('/catalogue/dune')
cy.get('[data-cy=borrow]').click()
cy.get('[data-cy=copies]').should('have.text', '2')
})go deeper
Recognise the message and know the quickest fix: delete the return, or move the read into a following step. You are not expected to recite the exact trigger conditions at this level.
Explain all three conditions that must hold — a command was queued, the return value is truthy, and it is not thenable — and why Cypress refuses rather than carrying a value computed before the queued work ran.
Show how you would diagnose this in a suite you did not write: the stack points at the chaining step, not the offending line, and the cure is usually to convert the read into a retried assertion rather than to shuffle callbacks.
Take a position on helpers that return values out of chains at all. Decide whether shared spec utilities may return a chain, must return nothing, or should be replaced by assertions the runner can retry.
This error catches a specific, very human mistake: writing a `.then()` callback as if it were an ordinary function that computes and returns an answer, while it is in fact still recording work for Cypress to do later. ## The exact condition Cypress instruments each `.then()` callback while it runs. Two facts are recorded: - **Did the callback enqueue at least one cy command?** - **What did the callback return?** The error fires only when the return value is **truthy**, has **no `.then` of its own**, and **at least one command was enqueued**. All three must hold. The message is fixed: > `cy.then()` failed because you are mixing up async and sync code. > > In your callback function you invoked 1 or more cy commands but then returned a synchronous value. > > You likely forgot to properly chain the cy commands using another `cy.then()`. ## Why Cypress refuses instead of guessing The queued commands have not run when your `return` statement executes. So the value you handed back was computed from the page **as it was before** the borrow button was clicked, before the reload, before whatever the queued commands are about to do. Cypress could silently accept it and carry a stale value forward, or it could stop and tell you. It stops, because the alternative is a test that reports a number nobody can trace back to a moment in time. ## What each return value does | The callback… | Return value | Result | |---|---|---| | queued a command | `undefined` (or no `return`) | accepted, no error | | queued a command | `null`, `0`, `''` | accepted; the value is falsy so the check does not fire | | queued a command | a string, number or jQuery object | **throws the mixing error** | | queued a command | something thenable | accepted; the check skips thenable values | | queued nothing | any value | accepted, no error | The two rows people trip on are the third and the fifth. A callback that only reads the DOM and returns a number is completely legal; adding one `cy.get()` above the `return` turns the same code into an error. ## Reading the failure in a real spec Suppose a helper in a library catalogue suite is meant to borrow a book and report how many copies are left: ```js // Throws: the callback queues a click, then returns a value computed before it. const borrowAndCount = () => cy.get('[data-cy=book-row]').then(($row) => { cy.get('[data-cy=borrow]').click() return Number($row.find('[data-cy=copies]').text()) }) ``` The stack points at `.then()`, but the cause is one line above the `return`. Three signals tell you it is this error and not something else: 1. The message names `cy.then()` and the phrase "mixing up async and sync code". 2. It ends with "The value you synchronously returned was:" followed by the value itself. 3. It is fully deterministic — it fails on the first run and every run, never intermittently. ## Chaining instead of returning The fix is to stop trying to hand a value back across the boundary and instead continue the chain, so the read happens **after** the queued work: - Replace the read with an assertion Cypress can retry: `cy.get('[data-cy=copies]').should('have.text', '2 copies available')`. This is the preferred form, because it is the only one that survives a slow render. - If a helper genuinely must produce a value, have it return the **chain** rather than a computed value, and let the caller add its own `.then()`. - If you only need a side effect, drop the `return` entirely — a callback that returns nothing is always accepted. ## The related error you may see instead There is a sibling message worth recognising, worded differently: *"Cypress detected that you returned a promise from a command while also invoking one or more cy commands in that promise."* That one is about wrapping cy commands inside your own promise rather than about returning a plain value, and it is thrown rather than warned because Cypress queues commands serially while a promise runs the moment it is created. Different trigger, same underlying lesson: **a `.then()` callback is a place to add more work to the chain, not a place to finish the job and hand back an answer.**
- Why does returning `undefined` from the same callback not raise the error?The check requires a truthy return value. A callback that returns nothing has not claimed to produce an answer, so there is no stale value for Cypress to object to. That is why deleting the `return` is often the whole fix.
- Is this error ever intermittent?No. It depends only on what the callback did and what it returned, both of which are decided by the code rather than by timing. If you are seeing it on some runs and not others, the callback is taking different branches, and the branch that queues a command is the one that throws.
saying these in an interview costs you the question
- Blames a timing race for a deterministic error
- Adds a fixed wait to make the error go away
- Says any return from .then() is illegal
- Thinks the returned value is simply stale, not fatal
- Reads the stack line as the real cause