---
title: "Good error messages"
url: "https://www.cypress.io/blog/good-error-messages"
description: "A better way to handle end to end assertion failures and display user-friendly messages with plenty of context."
---

# Good error messages

July 26, 2017

By Gleb Bahmutov

![An image of a code editor displaying CSS / SASS code](https://storage.ghost.io/c/12/89/12899ca1-4786-4eea-b7e1-9c5268baa48a/content/images/2025/04/photo-1519397154350-533cea5b8bff.jpeg)

If I can paraphrase a cliché:

> bad things happen to good programs

Things go wrong, even in the most carefully coded and thoroughly tested applications. In my opinion, a sign of a good app is if it gives meaningful and useful error messages when things go wrong.

At Cypress we spend a lot of time trying to make every type of error we display to the user as useful as possible. These include failed assertions during tests (after all, Cypress is an end to end test runner), and crashes inside our application.

## Useful unit test assertion failures

Let me start with showing an example of a typical failed test message that is NOT user friendly. Let’s use the [Mocha](https://mochajs.org/?ref=cypress-io.ghost.io) test runner and plain `console.assert` to verify that a result is equal to an expected value.

```js
it('checks a number', () => {
  const value = 3
  const expected = 3
  console.assert(value === expected)
})
```

The test passes; no big surprise there. Let us change the value to be “wrong” on purpose to see how the test reports its failure.

```js
it('checks a number', () => {
  const value = 10
  const expected = 3
  console.assert(value === expected)
})
```

The `npm test` command fails, and suggests that we look at the text above for details. Yet, aside from the stack frame where the assertion fails `Context.it (spec.js:4:11)`, there is almost no useful information! We have to open the file `spec.js` at line 4 ourselves, trace the values used in the `console.assert` expression and try to guess what went wrong in the test.

We can provide more information ourselves by using a library of _matchers_ - small functions that compare values and throw detailed error messages. A popular library like [chai](http://chaijs.com/api/bdd/?ref=cypress-io.ghost.io) can make our test code a lot more readable, and our test failure a lot more useful.

```js
const expect = require('chai').expect

it('checks a number', () => {
  const value = 10
  const expected = 3
  expect(value).to.equal(expected)
})
```

Yet, we could make another step towards more clarity in test output. Imagine we are comparing an item from an array with an expected value. Wouldn’t it be super useful to see the entire array automatically if the test fails? We could manually specify all the things to print in case of failure - but that is a long list! Or we could automatically transform the test code to instrument it. For example, the test runner [Ava](https://github.com/avajs/ava?ref=cypress-io.ghost.io) makes its assertions [more powerful](https://github.com/avajs/ava?ref=cypress-io.ghost.io#enhanced-assertion-messages) via the [power-assert](https://github.com/power-assert-js/power-assert?ref=cypress-io.ghost.io) module.

```js
import test from 'ava'

test('checks a number', t => {
  const values = [1, 2, 10]
  const expected = 3
  t.true(values[2] === expected)
})
```

Note the wealth of useful information shown automatically. It’s almost everything a developer might need to debug the failure:

-   Failed test source file and line number
-   The test source line itself
-   The expression that failed the `t.true` predicate
-   The left and right values in the strict equality comparison `values[2] === expected`
-   The “outer” values of all variables participating in the expression that failed. In our case the entire `values` array is printed

## Useful E2E assertion failures

Cypress.io is an end to end test runner and operates at the web application’s user interface level. So when a test fails there are plenty of things to record and display. A screenshot is taken automatically on failure, but we also pay close attention to the information coming from the matcher function. Our goal is to describe the expected outcome of the step that failed and to output all relevant UI information at the moment of failure.

Imagine testing the [TodoMVC](http://todomvc.com/?ref=cypress-io.ghost.io) application and checking if the ToDo list has 1 element. The end to end test looks something like this:

```js
beforeEach(() => {
  cy.visit('http://localhost:8888')
})
it('can insert new todo', () => {
  cy.get('.new-todo').type('first todo{enter}')
  cy.get('ul.todo-list').find('li').should('have.length', 1)
})
```

Let us change the test to expect 2 elements (but we still insert only 1).

```js
it('can insert new todo', () => {
  cy.get('.new-todo').type('first todo{enter}')
  cy.get('ul.todo-list').find('li').should('have.length', 2)
})
```

The test will fails, and the application will show the following:

> You can find the full set of Cypress TodoMVC tests in the [cypress-example-todomvc](https://github.com/cypress-io/cypress-example-todomvc?ref=cypress-io.ghost.io) repository.

Right away, the developer sees all the steps in the test in the [Command Log](https://on.cypress.io/test-runner?ref=cypress-io.ghost.io#Command-Log) on the left. Instead of the last stack frame, we see that the `GET ul.todo-list` step was successful. Then, after the `FIND li` step, it expected to find 2 elements in the list, but found just one.

We work hard to make sure if any of the many Cypress [API commands](https://on.cypress.io/api?ref=cypress-io.ghost.io) fail during the test it will generate similarly helpful message. Our goal is to give enough information to the developer so that they can diagnose the problem immediately by looking at the Command Log’s output, maybe even without rerunning the tests again.

Good error messages and recording the video of the browser is especially important when the tests execute on CI. You can store the videos as artifacts, just like the CircleCI is configured in the repo [cypress-example-todomvc](https://github.com/cypress-io/cypress-example-todomvc?ref=cypress-io.ghost.io).

Alternatively, you can send the screenshots and videos to Cypress and inspect the entire test run history in your [Dashboard](https://on.cypress.io/dashboard-service?ref=cypress-io.ghost.io). Just pass the `--record <key>` option when running on CI.

All the useful information for successful and failed tests in a single place.

## Useful CLI errors

Now let’s look at some typical server-side / CLI crashes. Imagine you run a program that tries to read a file.

```js
const fs = require('fs')
function foo() {
  fs.readFileSync('./important.json', 'utf8')
}
foo()
```

If the file does not exist, the CLI application crashes with a stack trace:

```text
$ node index.js
fs.js:640
  return binding.open(pathModule._makeLong(path), stringToFlags(flags), mode);
                 ^
Error: ENOENT: no such file or directory, open './important.json'
    at Error (native)
    at Object.fs.openSync (fs.js:640:18)
    at Object.fs.readFileSync (fs.js:508:33)
    at foo (/examples/simple/index.js:3:6)
    at Object. (/examples/simple/index.js:5:1)
    at Module._compile (module.js:570:32)
    at Object.Module._extensions..js (module.js:579:10)
    at Module.load (module.js:487:32)
    at tryModuleLoad (module.js:446:12)
    at Function.Module._load (module.js:438:3)
```

Note that a casual user _DOES NOT CARE ABOUT STACK TRACE_. Not a single bit. The user only cares about one thing:

> How do I make this work?

We, developers, might want a stack trace in the error report submitted by a user, but the user only needs to know what to do in case this error happens.

Some users _might_ read the error message and see the meaningful part of the error `no such file or directory, open './important.json'`, but that is rare. Even in this case, it is unclear what the user should do; is the file expected to be there by default? Or is the user supposed to create the file first?

There is a mismatch between what the author of the software wants: stack trace, verbose error information, and what the user wants: limited and actionable error message. This is why we have written [@cypress/error-message](https://github.com/cypress-io/error-message?ref=cypress-io.ghost.io) - a way to handle expected crashes and display a user-friendly message with plenty of context. In the same situation as above, we expect the possibility of a non-existent file and advise the user what to do, including reading information online.

```js
const {formErrorText} = require('@cypress/error-message')
// we expect the following error
const fileReadError = {
  description: 'Failed to read settings file',
  solution: `
    Please create file "important.json" with settings first,
    then run the program again.

    See more info at https://faq.company.com/setup
  `
}

const fs = require('fs')
function foo() {
  try {
    fs.readFileSync('./important.json', 'utf8')
  } catch (err) {
    formErrorText(fileReadError)(err).then(console.error)
  }
}
foo()
```

When running, this program shows a message the _the user will find useful_.

```text
$ node after.js
----------
Failed to read settings file

Please create file "important.json" with settings first,
then run the program again.

See more info at https://faq.company.com/setup

----------

ENOENT: no such file or directory, open './important.json'

----------
Platform: darwin
Version: 15.6.0
```

There is a human-friendly error description and a proposed solution, not your typical Node.js mumbo-jumbo.

If the `formErrorText` call syntax looks weird, it is because we curry the function to separate its configuration from error handling. This makes it very easy to plug message formatting into promise chains.

```js
const errorInformation = {
  description: '...',
  solution: '...'
}
fooAsync()
  .catch(formErrorText(errorInformation))
```

We open sourced this library on NPM as [@cypress/error-message](https://github.com/cypress-io/error-message?ref=cypress-io.ghost.io) and are using this internally in all our CLI code. The stack traces without meaningful messages should only happen in truly unexpected crashes.