skip to content

How far should a Cypress suite let `.then()` blocks replace retrying assertions?

level: principalimportance: should knowfreq 34%

answer

  1. Retry is the default, not the exception
  2. then buys JavaScript, costs the wait
  3. Once-only work belongs in then
  4. Pin the page before a one-shot block
  5. A longer timeout adds no retry

basics

~20 s

Only where the block must run exactly once. If it merely computes and asserts, put it in a should callback and keep the retry; reserve then for capturing values, queueing commands, and task or file work.

solid answer

~40 s

Treat the retry as the default and `.then()` as the exception you justify. A `.then()` block buys arbitrary JavaScript — derived totals, cross-element comparisons, branching — and pays for it with a one-shot check whose reliability depends on how fast the machine was that day. Almost every computed-value check living in `.then()` moves into `.should(cb)` unchanged and gains the retry for free. What genuinely belongs in `.then()` is once-only work: queueing further Cypress commands, `cy.task()` calls, capturing a value for a later step, writing a file. Where a block stays one-shot, pin the page with a retrying assertion immediately in front of it. The failure mode to watch for is a team raising `defaultCommandTimeout` instead — a longer timeout never adds a retry that is not there.

go deeper

for a junior

Learn the safe default before the nuance: if your block only looks at the page and asserts, write it as a should callback and let Cypress keep trying.

for a middle

Be able to name what a then block gives up — the retry and the re-query — and to list the once-only jobs that make a then block the correct choice anyway.

for a senior

Show that you can spot a missing retry in review rather than after a red build, and that you reach for the pinning assertion instead of a bigger timeout.

for a principal

Own the convention and its cost. Say where the line sits for this application, why it moves for a lazily hydrated page, and how you stop the timeout value creeping upward instead.

## What an explicit block buys, and what it charges An `expect()` block inside `.then()` is the most expressive thing in a Cypress spec. Inside it you have the whole language: parse a rate out of `$210.00`, multiply it by the nights the date picker spans, compare the rendered room list against a fixture, branch on whether the guest form showed a promo field. No chainer expresses those, and pretending otherwise produces worse tests than an explicit block does. The charge is timing. `.then()` runs its callback once, as soon as the subject is available, and freezes the chain above it — nothing re-queries. Every assertion inside is therefore a measurement of one instant. On a machine where the page has already settled, that instant is representative; on a loaded CI worker it is not, and the same block reports a defect that does not exist. So the question is never "is `.then()` allowed". It is: **what does this particular block need that a retried one cannot have?** ## The default position Start from a simple rule: *if the block only reads the subject, computes and asserts, it belongs in `.should(cb)`*. Moving it costs one word. You keep the arbitrary JavaScript, you keep the grouping, and you gain a check that survives a slow render. Most explicit blocks in a real hotel-booking suite are exactly that shape — the total equals rate times nights, the room list is sorted by price, the date picker's disabled days match the availability fixture. Teams that write this down usually phrase it as: a bare `expect()` inside `.then()` needs a reason visible in the diff. ## What genuinely stays in `.then()` 1. **Queueing further Cypress commands.** A retried callback may not contain one — Cypress throws — so a block that goes on to `cy.request()`, `.click()` or `cy.task()` is a `.then()` by construction. 2. **Capturing a value for later.** Reading the booking reference off the confirmation screen and holding it for a cleanup step is a once-only act by nature. 3. **Side effects.** `cy.writeFile()`, seeding through `cy.task()`, appending to a list the test inspects afterwards. Repeating any of these once per attempt is a bug. 4. **Branching on what actually rendered.** Choosing between two layouts is a decision, not an assertion, and it must be made once. Notice that none of those is an assertion. That is the pattern worth teaching: **`.then()` is for doing, `.should()` is for checking.** ## Pinning, and how far to take it Where a `.then()` block must stay one-shot but the page is still moving, the cheap discipline is a retrying assertion immediately in front of it: ```javascript cy.get('[data-cy=booking-total]') .should('not.contain', '—') .then(($total) => { // the assertion above pinned the page; this block opens on settled state }) ``` How far to take that is a genuine judgment call, and the answer moves with the application. A suite driving a deterministic, fixture-backed room search needs very little of it and reads better without the ceremony. A suite driving live pricing, animation and a lazily hydrated room list needs it nearly everywhere, and that team is better off simply declaring `.should(cb)` the only way a value check gets written. Two limits are worth naming out loud: - **Do not solve it with time.** Raising `defaultCommandTimeout` lengthens how long Cypress waits for a subject to appear; it adds no retry to a callback that runs once. A suite whose timeout keeps climbing is usually one that put its checks in the wrong callback. - **Do not solve it with size.** Collapsing twelve checks into one `.then()` block makes them no more reliable and makes the failure worse: the first throw ends the block, the remaining eleven never run, and one run tells you about one problem. ## What to agree once, as a team - Value checks default to `.should(cb)`; a `.then()` block carries a name or a comment saying what once-only work it is doing. - A `.then()` block containing nothing but `expect()` calls is treated in review as a missing retry. - Blocks stay small enough that the first failing line names the defect, with Chai's message argument on each assertion so the Command Log says which check broke. - The escape hatch — a genuinely one-shot check against genuinely settled state — stays available, and is written with the pinning assertion in front of it so the reasoning is visible in the code rather than in someone's memory. None of this is enforceable by a linter, which is exactly why it is worth deciding deliberately once instead of re-litigating it in every pull request.

  • What is the cheapest way to make an existing `.then()` assertion block reliable?
    Put a retrying assertion immediately before it so the block opens on settled state — `cy.get('[data-cy=booking-total]').should('not.contain', '—').then(...)`. It is a one-line change that leaves the block's structure alone. The fuller fix, preferable when the block does nothing but compute and assert, is to move the computation and the `expect()` calls into `.should(cb)` so the whole check retries.
  • How long should a single `.should()` callback be allowed to get?
    Short enough that its first failing line names the defect. Every `expect()` after the first throw never runs, so a twenty-line callback reports one problem and hides the rest. Label each assertion with Chai's message argument, and split a block that checks unrelated things into separate assertions on separate queries; grouping only earns its keep when the checks must see one consistent snapshot.

saying these in an interview costs you the question

  • Puts every assertion in one giant .then() block
  • Treats defaultCommandTimeout as a substitute for retrying
  • Bans .then() outright, including for cy.task() setup
  • Judges the choice by which reads nicer, not which waits