skip to content

Enqueue and Run Order

The spec body runs to the end first and only queues commands, which Cypress then drains one at a time. Asked because it explains almost every surprising ordering in a test.

on this pageshow

explore

questions

5

In a Cypress test, when do the cy commands in the test body actually run?

level: juniorimportance: must knowfreq 88%

answer

  1. Two phases, not one
  2. Calling is not running
  3. The body only describes the work
  4. Automation starts when the it() returns
  5. cy calls append to the command queue

basics

~20 s

Cypress commands do not run when you call them. Each cy call only appends a step to an internal command queue and returns immediately; Cypress drains that queue, one step at a time, after the test body function has returned.

solid answer

~50 s

A Cypress spec runs in two phases. In the first, the body of the Mocha `it()` executes top to bottom as ordinary synchronous JavaScript, and every `cy.visit()`, `cy.get()` or `.click()` you call does nothing except append a step to Cypress's command queue and return a chainable object. In the second, which begins only after that body returns, Cypress drains the queue and performs the browser automation, one step at a time, in the order the steps were appended. So a test that visits `/catalogue`, gets the search box and types into it has, at the moment its body finishes, navigated nowhere and touched no element — it has only described what it wants done. That is why plain JavaScript placed between two commands runs far earlier than it looks like it should, and why a synchronous `Cypress.$` lookup there finds nothing.

code

javascript · 17 lines
javascript
describe('library catalogue', () => {
  it('borrows the first matching book', () => {
    cy.visit('/catalogue')                    // 1. queued, nothing has loaded yet
    cy.get('[data-cy="catalogue-search"]')    // 2. queued
      .type('dune')                           // 3. queued
    cy.get('[data-cy="results"] .book-row')   // 4. queued
      .first()                                // 5. queued
      .find('[data-cy="borrow"]')             // 6. queued
      .click()                                // 7. queued

    // Cypress.$ is synchronous jQuery, so this line runs HERE,
    // during the body phase, before step 1 has run. It prints 0.
    console.log(Cypress.$('.book-row').length)

    cy.contains('[data-cy="shelf-status"]', 'Borrowed') // 8. queued
  })
})

go deeper

for a junior

Be ready to say in one sentence that a cy call only queues a step and that Cypress starts working once the test body returns. Interviewers open with this because everything else about the tool rests on it.

for a middle

Expect to walk a spec line by line and say which lines execute during the body phase and which during the drain, including any plain JavaScript sitting between two commands.

for a senior

Be able to use the two-phase model to explain a real failure someone brings you: a log that printed in the wrong place, a helper that appeared to do nothing, a loop that hung the browser.

for a principal

Be ready to say what the deferred model buys a suite and what it costs a team learning it, and what conventions you would set so people stop putting plain JavaScript between commands.

## Two phases, not one Every Cypress test runs in two distinct phases, and almost every surprising ordering in a spec comes from confusing them. The **body phase** is ordinary JavaScript. Cypress hands the function you passed to Mocha's `it()` to the runtime, and it executes top to bottom, synchronously, exactly like any other function. Nothing in the application under test is touched. The **drain phase** begins only after that function has returned. Cypress then walks the list of steps the body left behind and performs them one at a time, in the order they were appended. The Cypress documentation states the rule plainly: the runner does not kick off browser automation until the test function exits. ## What a `cy` call actually does When you write `cy.visit('/catalogue')`, the call does three things, and none of them is a visit: - It **creates a step** describing the work — which command, which arguments, which options. - It **appends that step** to a central command queue belonging to the current test. - It **returns a chainable object immediately**, so `.type()`, `.click()` or `.should()` can append the next step to the same chain. That is the whole of it. `cy.get('[data-cy="catalogue-search"]')` does not query the DOM. `.click()` does not dispatch an event. `cy.visit()` does not navigate. Each call records an intention and returns in microseconds, which is why a body containing forty commands finishes in well under a millisecond. The queue is not a promise chain either, even though `.then()` borrows the name. Cypress commands are serial steps passed into a queue and drained by the runner, and that is deliberate: the DOM is a highly mutable thing, so Cypress manages every interaction with it in a controlled, deterministic way rather than handing you a value you might hold on to. ## Walking a library-catalogue spec Consider a test that searches the catalogue and borrows the first result. During the body phase, Cypress appends, in order: 1. `cy.visit('/catalogue')` — navigate to the catalogue. 2. `cy.get('[data-cy="catalogue-search"]')` — find the search box. 3. `.type('dune')` — type into it. 4. `cy.get('[data-cy="results"] .book-row')` — find the result rows. 5. `.first()` — narrow to the first row. 6. `.find('[data-cy="borrow"]')` — find that row's borrow button. 7. `.click()` — click it. At the instant the body returns, **not one of those seven has happened**. Then the drain begins and they run in exactly that order, each finishing — including whatever waiting it needs — before the next one starts. Now drop a plain line between two of the commands: `console.log(Cypress.$('.book-row').length)`. `Cypress.$` is the jQuery build Cypress bundles, and it is synchronous, so it runs during the body phase, before the visit. It logs `0` on every run, against a page that has not been loaded yet. ## Body phase versus drain phase | | body phase | drain phase | |---|---|---| | when it happens | while the `it()` function is executing | after that function has returned | | what runs | plain JavaScript, `Cypress.$`, `console.log`, assignments | queued `cy` steps and the callbacks inside them | | how long it takes | microseconds | as long as the automation genuinely takes | | effect on the app | none at all | navigation, typing, clicking, assertions | | what a `cy` call does | appends a step | performs it | ## Consequences you will actually hit - **A log written between two commands prints before both of them.** It is in the body; they are in the queue. - **A helper that calls `cy` commands does not do the work when you call it.** It appends its steps at the point where you called it, and they run later, in that position. - **A JavaScript `while` loop around a `cy` command never gets anywhere.** The body must return before draining starts, so the loop appends steps forever and usually exhausts browser memory. Recursion driven from inside a `.then()` callback is the shape that works, because each pass is scheduled from within a step the runner is already draining. - **Code that must observe the page after a command belongs inside `.then()`.** `.then()` is itself a queued step, so its callback runs during the drain, at the position where you chained it. ## Why the model is built this way Deferring everything is what lets Cypress wrap behaviour around each step without you asking. A queued step is something the runner can time, wait on, time out and record: the visit can wait for the page's load event before the next step starts, and an action can wait until the element is in a state where acting on it makes sense. A command that executed the instant you called it would have no such envelope around it, and the test would be at the mercy of whatever the page happened to be doing in that microsecond. The cost is exactly what trips newcomers up: **the order of the lines you wrote is not the order in which things happen to the page**. It is the order of the queued steps, with every piece of plain JavaScript in the body pushed in front of all of them.

  • Does a JavaScript while loop that keeps calling cy.get() in a Cypress test body ever drive the browser?
    No. The body has to return before Cypress starts draining, so the loop just appends more and more steps and the automation never begins — the browser usually runs out of memory first. Recursion called from inside a `.then()` callback is the shape that works, because each pass is scheduled from within a step the runner is already draining.
  • Where do you put code that must read the page after a Cypress command has run?
    Inside a `.then()` callback. `.then()` is itself a queued step, so its callback executes during the drain phase, at the position where you chained it. Anything written between two `cy` calls at the top level of the test body runs during the body phase instead, which is before every one of them.

Writing a Cypress test is like filling out a work order rather than doing the job: the body of the test writes every line of the order, and only when you hand it over does the runner start working through it in sequence.

saying these in an interview costs you the question

  • Says cy.get() returns the matching element right away
  • Thinks each cy line finishes before the next line is reached
  • Assumes a console.log between two commands prints between them
  • Believes a JavaScript loop around cy.get() will drive the browser
open as a page

When a Cypress command fails, what happens to the commands still queued behind it?

level: seniorimportance: must knowfreq 56%

basics

~20 s

They never run. A failed Cypress command ends the test — everything still queued behind it is abandoned and the test is reported failed. Cypress offers no catch handler on a command, so a step cannot recover and carry on.

open as a page

Why does Cypress drain its command queue serially instead of running commands in parallel?

level: middleimportance: should knowfreq 52%

basics

~20 s

Most Cypress commands mutate browser state — they navigate, set cookies, fire clicks. Running them at once would make a result depend on timing, so Cypress drains its queue one step at a time, each finishing before the next begins.

open as a page

In Cypress, what happens if a .then() callback returns a Cypress.Promise?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Cypress bundles Bluebird and exposes it as Cypress.Promise. When a queued step's callback returns one, Cypress holds the drain and does not start the next queued command until that promise settles, so real async work fits inside the queue's order.

open as a page

In Cypress 16, what replaces the removed cy.end() command?

level: middleimportance: nice to knowfreq 19%

basics

~20 s

Nothing replaces it. cy.end() ended a chain by yielding null, which was never needed, because a Cypress chain is already terminated as soon as the next cy command starts a new one. In Cypress 16 you simply delete the calls.

open as a page