Skip to content
SDETRole
SDETRole

Cypress interview question

Why can't you use async/await with Cypress commands?

intermediate 2–5 yrs #command-queue#async#chainables#then

Short answer

Cypress commands are not promises. Calling cy.get() enqueues a command and returns a chainable immediately; Cypress runs the queue later with retries. Use .then() to work with yielded values, aliases to share them, and cy.wrap() for real promises, instead of await.

Answer

This question tests whether you understand Cypress’s execution model, which is the single biggest source of confusion for people coming from Selenium or Playwright.

Commands are enqueued, not executed

When your test function runs, each cy.* call does not perform the action. It adds a command to a queue and immediately returns a chainable object. Only after the whole test function has finished does Cypress start running the queue, one command at a time, with retries and timeouts.

it('demonstrates queueing', () => {
  console.log('1');
  cy.visit('/products');
  cy.get('[data-cy=product]').should('have.length', 10);
  console.log('2');
});
// Console prints "1" then "2" before the page has even loaded.

Chainables are not promises

A chainable has a .then() method, so it looks promise-like, but it is not a real Promise. Awaiting it does not give you the element, and mixing await with the queue leads to commands running out of order or Cypress warnings about returning a promise while also invoking commands. Cypress also cannot retry something that has already resolved to a single value.

// Wrong: text is not a string, and the command has not run yet
const text = await cy.get('[data-cy=total]').invoke('text');

// Wrong: same problem without await
let total;
cy.get('[data-cy=total]').invoke('text').then(t => { total = t; });
expect(total).to.eq('$30.00'); // runs before .then, total is undefined

The right patterns

Assertions that retry

Most of the time you don’t need the value; you need an assertion, and .should() retries the query until it passes:

cy.get('[data-cy=total]').should('have.text', '$30.00');

.then() for computed logic

cy.get('[data-cy=price]').invoke('text').then(priceText => {
  const price = Number(priceText.replace('$', ''));
  cy.get('[data-cy=qty]').clear().type('3');
  cy.get('[data-cy=total]').should('have.text', `$${(price * 3).toFixed(2)}`);
});

Note that .then() does not retry, so put the retrying assertion inside it.

Aliases to share values

beforeEach(() => {
  cy.request('POST', '/api/orders', { sku: 'ABC' }).its('body.id').as('orderId');
});

it('opens the order', function () {   // function(), not arrow, to use this.orderId
  cy.visit(`/orders/${this.orderId}`);
});

cy.wrap() for real promises

If you have an actual Promise (from a helper library), cy.wrap(promise) puts it into the queue so Cypress waits for it in order.

Why Cypress chose this design

The queue enables automatic retries of queries, consistent command logging and DOM snapshots for time-travel debugging. The cost is that test code reads synchronously but does not run synchronously.

What interviewers look for

  • “Commands are queued and run later” stated in the first sentence.
  • Knowing chainables are not promises and why that enables retry-ability.
  • Fluent use of .should(), .then(), aliases and cy.wrap().

Common mistakes

  • Using arrow functions with this.alias.
  • Putting a non-retrying expect in .then() when a .should() was needed, which reintroduces flakiness.

A strong answer covers

  • Commands are enqueued and run later, not executed immediately
  • Commands return chainables, not promises
  • Use .then(), aliases (.as) or cy.wrap to access values
  • Retry-ability would break if values were resolved once
Practise answering under time pressure

Type to search across SDETRole.

Full-text search by Pagefind · runs in your browser Open search page →