Testing Exceptions and Errors

As much as we would love to pretend our code never throws an error, they often do. And it is very important to remember to test for this in tests.

(It is often known as testing the 'unhappy path').

In Jest or Vitest it is quite simple to test, although you often have to wrap your code in another function to assert that an error was thrown.

(Otherwise the entire test will just fail).

Simple example of a function that throws

If you have a function that throws an error, and you need to check that happens then you can pass it to expect(), and add a .toThrow()

Note: in this example I'm passing the function itself- I am not calling it.

Jest or Vitest will see that it is a function, and when combined with .toThrow() it will run the function and check if it was thrown.

This is a little unrealistic, as you will often need to pass arguments. We will get to that!

function alwaysThrows() {
  throw new Error(
    'This is always thrown'
  );
}

test('always throws an error', () => {
  // note: this is not calling alwaysThrows()
  expect(alwaysThrows).toThrow(
    'This is always thrown'
  );
});

Testing a function that throws with arguments

It is much more likely you need to pass arguments to a function.

To check if it throws, wrap the call in a function and pass that wrapper function.

function maybeThrows(shouldWeThrow) {
  if (shouldWeThrow)
    throw new Error('oh no');
}

test('it throws when passed a value', () => {
  expect(() =>
    maybeThrows(true)
  ).toThrow('oh no');

  // which is the same as:
  const fn = () => maybeThrows(true);
  expect(fn).toThrow('oh no');
});

Matching the type of error

You don't actually have to pass anything to toThrow().

This will pass

test('it throws when passed a value', () => {
  expect(() =>
    maybeThrows(true)
  ).toThrow();
});

But sometimes you need to check the exact thing that was thrown.

Matching the class that was thrown

If you throw different classes such as class SyntaxError, the easiest way is to do toThrow(SyntaxError):

class SyntaxError extends Error {}
class LengthError extends Error {}

function checkCode(code) {
  if (code === 'syntax-error')
    throw new SyntaxError();
  if (code.length > 5)
    throw new LengthError();
}

test('throws a syntax error', () => {
  expect(() =>
    checkCode('syntax-error')
  ).toThrow(SyntaxError);
});

test('throws a length error', () => {
  expect(() =>
    checkCode('very long string')
  ).toThrow(LengthError);
});

Matching error strings

You can also pass in a string and it will match it against the Error message

function checkCode(code) {
  if (code === 'syntax-error')
    throw new Error(
      'You have a syntax error'
    );
  if (code.length > 5)
    throw new Error('Too long');
}

test('throws a syntax error', () => {
  expect(() =>
    checkCode('syntax-error')
  ).toThrow('You have a syntax error');
});

test('throws a length error', () => {
  expect(() =>
    checkCode('very long string')
  ).toThrow('Too long');
});

If doing string comparisons, you can also pass a substring, so these will also pass:

function checkCode(code) {
  if (code === 'syntax-error')
    throw new Error(
      'You have a syntax error'
    );
  if (code.length > 5)
    throw new Error('Too long');
}

test('throws a syntax error', () => {
  expect(() =>
    checkCode('syntax-error')
  ).toThrow('syntax');
});

test('throws a length error', () => {
  expect(() =>
    checkCode('very long string')
  ).toThrow('long');
});

Regex matches

Because substring matches are straightforward (see previous examples), regex is rarely required. But for more complex patterns you can easily pass in a regex.

test('throws error with complex regex pattern', () => {
  expect(() =>
    checkCode('syntax-error')
  ).toThrow(/You have a \w+ error/);
});

Testing errors for async behaviour

We will cover this in more detail in a lesson, but if you want to know now, here is an example. You use await before the expect(), and then .rejects.toThrow(YourError)

async function fetchUserData(userId) {
  if (!userId) {
    throw new Error(
      'User ID is required'
    );
  }
}

test('should throw error for missing user ID', async () => {
  await expect(
    fetchUserData()
  ).rejects.toThrow(
    'User ID is required'
  );
});

Lesson Task

There are a bunch of tests that need completing. You will need to call the functions which may or may not throw an error, and assert .toThrow('some error') on them.

Remember you often need to wrap the function call in another function (so Jest/Vitest can catch the error)

For the final incomplete test, make sure a specific error prototype is thrown. For this you can pass in .toThrow(SomeError).

Ready to try running tests for this lesson?