- Published on
Testing Exceptions in Jest - stop using try/catch
- Authors

- Name
- Emanoel Oliveira
TL;DR: Using
try/catchto 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:
- It silently lies: if
service.signIn(input)doesn't throw any exception, thecatchblock never executes — and neither does theexpectinside it. The test passes without validating anything at all. - 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
- https://github.com/goldbergyoni/javascript-testing-best-practices
- https://jestjs.io/docs/expect#rejects
- https://jestjs.io/docs/expect#tothrowerror