ScreenshotNeo

BlogHow-to

How to Write Helpful Error Messages in Cypress Tests

Add clear labels to Cypress assertions so failures explain which expected behavior needs attention, while keeping Cypress’s retry behavior intact.

By the ScreenshotNeo team4 October 20265 min read

To add context to a Cypress assertion failure, pass a short message as the second argument to Chai’s expect inside a .should() callback. Cypress documents that these messages appear in the Command Log beside the assertion. Label the behavior or element being checked, and keep the callback limited to repeatable assertions.

cy.get('[data-testid="todos"]').should(($todos) => {
  expect($todos, 'todo list after adding one item').to.have.length(3)
  expect($todos, 'new todo is visible in the list').to.contain('Write tests')
})

The label adds diagnostic context; it does not change what the assertion checks or disable Cypress retries. Exact output can vary with Cypress, Chai, and reporter versions.

1. What a useful assertion message says

A useful label tells you which expected behavior, item, or condition the assertion represents. For example, confirmation after submitting the form gives more context than should contain, which mostly repeats assertion mechanics.

cy.get('[data-testid="submit"]').click()

cy.get('[data-testid="confirmation"]').should(($confirmation) => {
  expect($confirmation, 'confirmation after submitting the form')
    .to.contain('Your request was received')
})

Keep labels short and specific. If the test title and assertion already make the expected behavior obvious, an extra label may add little. Cypress’s .should() API documentation describes passing a string as the second argument to expect for this purpose.

2. Keep Cypress retries working for you

Cypress retries assertions in .should() until they pass or time out. A callback passed to .should() can run more than once, so its contents must be safe to repeat. Keep it focused on assertions: do not put external side effects or Cypress commands inside the callback.

// Good: both checks inspect the yielded subject and may be retried.
cy.get('[data-testid="results"]').should(($results) => {
  expect($results, 'results list is visible').to.be.visible
  expect($results, 'results include the saved item').to.contain('Saved report')
})

If conditions are independent and read more clearly as separate steps, use separate queries and assertions. Avoid turning one callback into an opaque collection of unrelated checks. A label annotates an expectation; it does not replace Cypress’s retry model.

3. Assert the intended outcome, not merely a change

A clear error message cannot make an ambiguous assertion correct. Negative assertions can pass for unintended reasons. After adding a todo, for instance, not.have.length(2) could pass because the application removed an item or inserted a blank one. Assert the result the user needs instead:

cy.get('[data-testid="add-todo"]').click()

cy.get('[data-testid="todos"]').should(($todos) => {
  expect($todos, 'todo list contains the expected number of items').to.have.length(3)
  expect($todos, 'new todo appears after adding it').to.contain('Write tests')
})

Use a negative assertion when absence itself is the behavior and other incorrect states are controlled. Otherwise, a positive assertion of the expected content or state usually makes the test’s purpose clearer. See Cypress’s guidance on assertions.

4. Choose selectors that match the behavior

Whether a wording change should fail the test is a useful guide to selector choice:

  • Visible text is part of the contract: select by text when changing that wording should make the test fail.
  • Copy is incidental: select with a stable data attribute when a wording edit should not break a behavior test.

This keeps failures focused on the behavior under test. Cypress discusses this distinction in its best practices.

5. Read the full failure report

Read a failed assertion as a sequence: the error type and message, the assertion label, expected and actual values, the source code frame, and any stack trace or documentation link. The custom label supplements that information; it does not replace it. Output details can vary by Cypress version, browser, and reporter.

Cypress’s engineering article on test error code frames explains why readable, actionable failure output matters. Its older article “Good error messages” describes the goal of showing the expected outcome and relevant UI information at failure time; treat it as historical context rather than a guarantee of today’s report format.

6. Common problems and fixes

Problem Likely cause Fix
The label is missing from the output The message was not passed as the second argument to Chai expect, or the assertion is not in the documented callback pattern. Use expect(subject, 'short behavior label') inside .should((subject) => { ... }); check the installed Cypress and reporter versions if formatting matters.
The callback causes duplicate actions A retried .should() callback contains a side effect or command. Keep the callback limited to repeatable assertions. Put actions in normal Cypress command steps outside it.
The test passes although the feature is broken A broad negative assertion is true for an unintended state. Assert the expected item, count, or state directly, and control other possible failure modes.
A harmless copy edit breaks the test The test selects by text even though wording is not part of the behavior. Use a stable data attribute when copy is incidental. Keep text selection when the wording itself is contractual.
The label repeats the assertion without helping It describes low-level mechanics rather than the expected behavior. Name the result or element in context, such as confirmation after submitting the form.

7. Or skip the browser setup

If a test or workflow also needs a rendered webpage screenshot, ScreenshotNeo provides a one-request screenshot API. It is a website screenshot API and MCP server from ScreenshotNeo; the request options are documented at ScreenshotNeo docs.

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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
  • Cookie banners are accepted and removed before capture; known consent platforms, newsletter popups, and chat widgets are removed too. Each step can be turned off.
  • Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing. Response headers report the page verdict and billing status.
  • An MCP server lets AI agents using Claude, Cursor, or another MCP client call take_screenshot, get_page_info, and capture_pdf.
  • The Free plan includes 1,000 screenshots each month with no card. Paid plans start at $5 for 3,000 shots.

Sign up for 1,000 free screenshots a month with no card.

8. FAQ

Does a custom label replace Cypress’s expected and actual values?

No. It adds context to the assertion in the Command Log; inspect the rest of the failure output too.

Should every assertion have a custom message?

No. Add a label where it clarifies which expectation failed, especially when several checks share a subject.

Can I put Cypress commands inside a .should() callback?

No. Keep Cypress commands and side effects outside the callback, which Cypress may retry.

Does a custom message make an assertion more reliable?

No. It improves context in the failure report. Reliability comes from asserting the correct, specific behavior and using Cypress’s retryable assertions appropriately.