Q34 of 48 · Cypress

How does Cypress handle asynchronous code under the hood?

CypressSeniorcypressinternalscommand-queuesenior

Short answer

Short answer: Cypress maintains an internal command queue. Each `cy.*` call appends to the queue synchronously, returning a chainable. After the test function returns, the runner drains the queue — executing each command, awaiting its async work, snapshotting state, then moving to the next.

Detail

Reading Cypress's source helps demystify the model. Internally, the framework holds a per-test command queue. Each command is a { name, args, fn } record. cy.get doesn't query the DOM at call time; it pushes a command onto the queue and returns a Chainable proxy.

When the test callback finishes synchronously, Cypress runs the queue:

  1. Pop the first command.
  2. Invoke its function with the previous subject as input.
  3. The function returns a value or a promise.
  4. If a promise, Cypress awaits it (with the appropriate timeout).
  5. Pass the result as the subject to the next command.
  6. Snapshot the DOM if the command is "snapshottable" (most are).

This explains several otherwise-puzzling behaviours:

Retry-ability is implemented in the query layer. cy.get and assertions both internally retry the underlying jQuery query until it succeeds or the timeout fires. The chain itself doesn't loop — the individual command does.

.then exits the queue because its callback runs synchronously with the resolved subject; the callback's return value becomes the new subject. If you return a Cypress chain from inside .then, Cypress re-enters the queue.

Promises don't compose with the queueawait fetch(...) from inside a test runs on a different timeline than the Cypress queue. The fix is cy.wrap(fetchPromise), which adds the promise to the queue.

async test functions break things because Cypress sees the test return a promise and either ignores it (the test continues) or treats it as a command to await (depending on version). The recommended pattern is a synchronous test body that just enqueues commands.

The senior signal: knowing the queue is internal and the test code is just declarative. You don't write async tests; you build a command list that Cypress executes.

// WHAT INTERVIEWERS LOOK FOR

Knowing the queue model, that commands are enqueued not executed, and the consequence: `async`/`await` from the test fights the framework. Bonus for the snapshot mechanism.

// COMMON PITFALL

Saying 'Cypress uses promises' — it returns Chainables that look promise-like but aren't. Mixing real promises with the queue is where bugs come from.