How to Use Cypress should() Assertions
Learn how Cypress .should() assertions work, how retries and subjects affect chained commands, and when to use .then() instead.
Cypress .should() attaches an assertion to a command that yields a subject. Cypress retries linked queries and the assertion until it passes or the applicable timeout expires. Use a callback to group repeat-safe checks against the same subject; use .then() for one-time work. A callback passed to .should() may run repeatedly and must not contain Cypress commands or side effects.
1. Start with a command that yields the subject
You cannot call .should() directly from cy. Chain it after a command that yields the element or value you want to check:
cy.get('.error').should('be.empty')
cy.contains('Login').should('be.visible')
cy.wrap({ foo: 'bar' }).its('foo').should('eq', 'bar')
.should() is an alias of .and(). These forms are supported:
.should(chainers)
.should(chainers, value)
.should(chainers, method, value)
.should(callbackFn)
2. Understand retry behavior
Cypress links queries together and retries them when an assertion fails. For example, if an element has not appeared yet, cy.get() can keep querying while .should('be.visible') checks it. Cypress retries until the assertion passes or the applicable timeout expires. The timeout depends on configuration and command options; do not assume every assertion always waits exactly ten seconds.
// Wait for the matching element to exist and become visible.
cy.get('[data-testid="save-status"]')
.should('be.visible')
.and('contain', 'Saved')
Queries and assertions retry as a linked chain. A one-time action does not become retryable just because an assertion follows it. Keep actions such as clicks outside a retrying assertion callback.
3. Use callback assertions for related checks
A callback is useful when multiple conditions should be checked against the same refreshed subject. If an assertion throws, Cypress invokes the callback again until it succeeds or times out:
cy.get('[data-testid="random-number"]').should(($div) => {
const n = parseFloat($div.text())
expect(n).to.be.gte(1).and.be.lte(10)
})
Keep this callback synchronous, repeat-safe, and limited to reading the subject and making assertions. Do not mutate the page, click, perform logging commands, or call Cypress commands inside it. Cypress commands in a .should() callback are unsupported. Issue commands before or after the assertion instead.
4. Choose chainers and know what they yield
Cypress bundles Chai and provides Chai-jQuery and Sinon-Chai extensions. Use an available chainer for standard DOM and value checks; use a callback with expect for custom conditions.
// Common documented patterns
cy.get('.left-nav > .nav').children().should('have.length', 8)
cy.get('#header a').should('have.attr', 'href', '/users')
cy.get('nav').should('be.visible')
Set expected values from your application’s requirements rather than copying example counts. Prefer assertions that describe the actual required state. A broad negative assertion can pass in multiple unintended states, so be specific about what should be absent or present.
Most assertions yield the same subject they received, but some chainers yield a value instead. For example, should('have.css', 'font-family') yields the CSS value, and should('have.attr', 'href') yields the attribute value. Check the chainer’s yielded value before passing it to a later command that expects a particular subject type.
5. Avoid stale elements after a rerender
A passing assertion partway through a query chain creates a retry boundary. If a later query fails, Cypress does not rerun queries before that passing assertion. If the application rerenders meanwhile, a later command can receive a detached, stale DOM element.
When later work needs the current DOM, start a new chain from the page root and find the element again:
cy.get('.list').find('li').eq(2).should('contain', 'Header')
// Query again so this assertion starts from the current DOM.
cy.get('.list')
.find('li')
.eq(2)
.children('.child')
.eq(3)
.should('contain', 'child')
You can also keep related observations inside one callback assertion when its code is safe to run more than once. A callback’s return value is ignored: the original subject continues down the chain.
6. Use .should() or .then() deliberately
| Use | Behavior | Callback guidance |
|---|---|---|
.should() |
Retries the linked query and assertion until success or timeout. | For assertions; safe to repeat and contains no Cypress commands. |
.then() |
Runs once after the prior command settles; it does not retry the earlier query. | For one-time handling, manipulation, or follow-up work. |
If the UI may still be updating, assert with .should() rather than using .then() as a waiting mechanism. A common pattern is to wait for the required state with .should(), then do one-time work in a following .then().
cy.get('[data-testid="save-status"]')
.should('contain', 'Saved')
.then(($status) => {
// One-time handling after the assertion passes.
const message = $status.text()
cy.log(`Confirmed status: ${message}`)
})
7. Troubleshoot common assertion failures
| Symptom | Likely cause | Fix |
|---|---|---|
Cannot read properties of undefined or no subject |
.should() was called without a preceding subject-yielding command. |
Start with a query or value command such as cy.get(), cy.contains(), or cy.wrap(). |
| Assertion times out while the UI changes | The query did not find the expected state before its applicable timeout; the selector, expected state, or timeout may be wrong. | Check the selector and application requirement, and configure an appropriate timeout where needed. |
| Callback runs more than once | This is normal retry behavior when an assertion has not passed. | Remove side effects and Cypress commands from the callback; keep it to synchronous reads and assertions. |
| Command inside callback is unsupported | A Cypress command was placed inside a retryable .should(callback). |
Move the command before or after .should(); use .then() for one-time work. |
| Detached element or later query fails after a passing assertion | A rerender replaced the DOM node after the assertion locked in its subject. | Start a fresh chain from cy.get() and locate the current element again. |
| Later command receives a string instead of an element | A chainer such as have.attr or have.css yielded the attribute or CSS value. |
Check the chainer’s subject behavior and use a new DOM query if the next step needs an element. |
| Negative assertion passes unexpectedly | The assertion describes several states, including unintended ones. | Assert the positive state the test requires, or make the negative condition more specific. |
8. Keep tests reliable and efficient
- Assert the state that matters to the user or application requirement, not incidental text or a copied example value.
- Use retryable queries and assertions for asynchronous UI state rather than fixed delays where a state assertion can express the wait.
- Keep callback assertions short and repeat-safe so retries do not duplicate work.
- Re-query after possible rerenders when a later step depends on a fresh DOM node.
- Set a longer timeout only when the application legitimately needs more time; a longer timeout cannot fix an incorrect selector or impossible expected state.
The primary cost of a poor assertion is usually slower feedback and flaky tests: unnecessary waits delay the suite, while one-time checks against changing state can fail intermittently. Cypress retry behavior helps absorb normal UI timing variation, but it cannot make an incorrect assertion or stale subject reliable.
9. Or skip the browser setup
If you also need screenshots of pages for documentation, visual review, or an agent workflow, ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. Its API takes one GET request and can return PNG, JPEG, WebP, or PDF. See the API documentation for the available parameters.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
- Cookie banners are accepted and removed before capture, along with known consent platforms, newsletter popups, and chat widgets; each step can be turned off.
- Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed. Responses include
X-Page-VerdictandX-Billedheaders. - An MCP server provides
take_screenshot,get_page_info, andcapture_pdftools for AI agents and MCP clients. - The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.
Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.
10. FAQ
Can I use multiple .and() assertions?
Yes. .and() is an alias of .should(), so you can chain assertions against the yielded subject. Be mindful that some chainers change the subject.
Does returning a value from a .should() callback change the chain?
No. The callback’s return value is ignored, and the original subject continues.
Should I put a click inside a .should() callback?
No. The callback may run repeatedly, so a click could happen more than once. Perform the action as a separate Cypress command and assert the resulting state.


