Cypress interview question
Why can't you use async/await with Cypress commands?
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 andcy.wrap().
Common mistakes
- Using arrow functions with
this.alias. - Putting a non-retrying
expectin.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