Logo
Published on

Testing Exceptions in Jest - stop using try/catch

Authors
  • avatar
    Name
    Emanoel Oliveira
    Twitter

TL;DR: Using try/catch to test exceptions in Jest is a dangerous anti-pattern — if no exception is thrown, the test passes silently. Use .rejects.toThrow() for a correct, concise, and safe test.

Introduction

Testing whether a function throws an exception seems straightforward, but there's a very common anti-pattern in the Jest ecosystem that can leave tests passing even when the code is broken.

Let's use an authentication service that implements a simple login as our example:

// auth.service.ts
async signIn({ email, password }: SigninDto): Promise<SignResponseDto> {
	const user = await this.usersService.findOne({ email });

    if (!user || !(await bcrypt.compare(password, user.password))) {
      throw new UnauthorizedException('Invalid Credentials');
    }

    const token = await this.jwtService.signAsync({ sub: user.id });

	return { id: user.id, name: user.name, token };
}

The Problem

When writing the error scenario which throws an UnauthorizedException it's very common to see the following anti-pattern:

it('should throw exception when user does not exists', async () => {
  try {
	jest.spyOn(userServiceMock, 'findOne').mockResolvedValueOnce(null);
	await service.signIn(input);
  } catch (error) {
	expect(error).toBeInstanceOf(UnauthorizedException);
  }
});

This pattern has two serious problems:

  1. It silently lies: if service.signIn(input) doesn't throw any exception, the catch block never executes — and neither does the expect inside it. The test passes without validating anything at all.
  2. It's verbose: 5 lines to do something Jest already offers natively.

The Solution

Instead, the recommended approach is to chain two Jest methods: .rejects and .toThrow.

With these two methods, the same test can be reduced considerably while also making more semantic sense when reading the code:

it('should throw exception when user does not exists', async () => {
  jest.spyOn(userServiceMock, 'findOne').mockResolvedValueOnce(null);
  await expect(service.signIn(input)).rejects.toThrow(
	UnauthorizedException,
  );
});

Going Deeper

.rejects

.rejects waits for a promise to be rejected and exposes the rejection reason for chained assertions.

If the promise resolves instead of rejecting, the test fails with the message Received promise resolved instead of rejected — guaranteeing the test only passes when the exception actually occurs.

.toThrow(error?)

The toThrow method is used to test thrown errors. It accepts an optional argument to validate specific errors:

Regular Expression

Validates if the error message matches the provided pattern:

await expect(service.signIn(input)).rejects.toThrow(
	/Invalid/,
);

String

Validates if the error message includes the substring:

await expect(service.signIn(input)).rejects.toThrow(
	'Invalid Credentials',
);

Error Object

Validates if the error message equals the object's message property:

await expect(service.signIn(input)).rejects.toThrow(
	new UnauthorizedException('Invalid Credentials'),
);

Error Class

Validates if the error object is an instance of the class:

await expect(service.signIn(input)).rejects.toThrow(
	UnauthorizedException,
);

Beyond being shorter and more readable, this pattern eliminates the risk of false positives — the test only passes when the correct exception is thrown.

References

Subscribe to the newsletter