Why Test for Thrown Errors
Well-designed functions often throw deliberately: rejecting invalid input, guarding against impossible states, or signaling a failed network call. If you never write a test that exercises the error path, that path can silently rot as the codebase evolves, throwing the wrong message, the wrong error type, or nothing at all. Jest's toThrow matcher lets you assert that a function call raises an exception and, optionally, that the exception matches an expected message or type, turning error handling into something the test suite actively protects.
Cricket analogy: Testing the error path is like a team practicing what happens when a review (DRS) is used and overturned, not just rehearsing routine deliveries; the rare event still needs a drilled, correct response.
Using toThrow
toThrow requires the code under test to be wrapped in a function, typically an arrow function passed to expect, because Jest needs to invoke that function itself inside a try/catch to observe whether it throws. Calling the function directly inside expect(), like expect(parseConfig(badInput)).toThrow(), executes it immediately during the expect call, so the exception is thrown before Jest's matcher machinery ever gets a chance to catch it, and the test simply crashes.
Cricket analogy: Wrapping the call in a function is like a coach not swinging the bat themselves but instead setting up a bowling machine to deliver the ball on command, so the observer can watch and judge the outcome safely.
function withdraw(balance, amount) {
if (amount > balance) {
throw new Error('Insufficient funds');
}
return balance - amount;
}
test('throws when withdrawing more than the balance', () => {
expect(() => withdraw(100, 150)).toThrow();
expect(() => withdraw(100, 150)).toThrow('Insufficient funds');
});
// WRONG: this throws immediately, outside Jest's control
// expect(withdraw(100, 150)).toThrow();Always wrap the code under test in an arrow function, e.g. expect(() => fn(arg)).toThrow(), never expect(fn(arg)).toThrow(). Calling the function eagerly means the exception is thrown before toThrow can intercept it, and the test fails with an uncaught error instead of a matcher failure.
Matching Specific Error Messages and Types
toThrow accepts several optional arguments that narrow what counts as a match: a string checks that the error message contains that substring, a regular expression checks that the message matches the pattern, and an Error class or constructor checks that the thrown value is an instance of that class. Passing no argument at all only verifies that something was thrown, which is a weaker guarantee, since a completely unrelated bug could throw a different, unexpected error and still pass the test.
Cricket analogy: It's like distinguishing 'the batter got out' from 'the batter got out specifically bowled by a yorker'; the general dismissal check is weaker than confirming the exact mode of dismissal you expected.
For custom error classes, use expect(() => fn()).toThrow(InsufficientFundsError) to assert both the error type and, combined with a message argument like toThrow(new InsufficientFundsError('Insufficient funds')), the exact message too.
Testing Async Rejections vs. Sync Throws
toThrow only works for synchronous exceptions; if a function returns a rejected promise instead of throwing directly, wrapping it in an arrow function and calling toThrow will not catch the rejection, because the function returns normally (with a promise) rather than throwing. For async code, combine Jest's promise matchers with toThrow: expect(promise).rejects.toThrow() awaits the promise and asserts that it rejects with a matching error, which is the correct pattern for testing async functions or functions that return rejected promises.
Cricket analogy: It's like the difference between a batter being given out immediately by the umpire (sync throw) versus a decision that only gets confirmed after a DRS review completes some seconds later (async rejection); you need to wait for the review before checking the outcome.
async function fetchUser(id) {
if (id < 0) {
throw new Error('Invalid id');
}
return { id, name: 'Ada' };
}
test('rejects.toThrow catches an async throw', async () => {
await expect(fetchUser(-1)).rejects.toThrow('Invalid id');
});- toThrow requires the code under test to be wrapped in a function, e.g. () => fn().
- Calling the function directly inside expect() throws before Jest can catch it.
- toThrow() with no argument only checks that something was thrown, which is a weak assertion.
- toThrow(string) checks for a substring match; toThrow(/regex/) checks a pattern match.
- toThrow(ErrorClass) asserts the thrown value is an instance of that error type.
- toThrow alone does not work for promise rejections; use expect(promise).rejects.toThrow() for async code.
- Testing error paths protects against regressions in validation and failure-handling logic.
Practice what you learned
1. Why must the code under test be wrapped in an arrow function when using toThrow?
2. What does expect(() => fn()).toThrow() with no arguments verify?
3. Which is the correct way to test that an async function rejects with an error?
4. expect(() => validate(input)).toThrow('required') checks what?
5. What happens if you write expect(parseConfig(badInput)).toThrow() and parseConfig throws synchronously?
Was this page helpful?
You May Also Like
Common Matchers: toBe, toEqual, toContain
Learn how Jest's core matchers toBe, toEqual, and toContain compare values, objects, and collections, and when to reach for each one.
Testing Async Code: Promises and async/await
Learn the correct patterns for testing promise-based and async/await code in Jest, and the pitfalls that let async bugs slip past a green test run.
Truthiness and Number Matchers
Master Jest's matchers for null, undefined, and boolean-like values, plus numeric comparisons including safe handling of floating-point precision.