100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Testing

Testing Exceptions with toThrow

Learn how to assert that a function throws an error using toThrow, including matching messages, error types, and async rejections.

Assertions & MatchersIntermediate8 min readJul 10, 2026
Analogies

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.

javascript
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.

javascript
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

Was this page helpful?

Topics covered

#Testing#JestStudyNotes#TestingQA#TestingExceptionsWithToThrow#Exceptions#ToThrow#Test#Thrown#ErrorHandling#StudyNotes#SkillVeris