
When working with Jest, handling errors efficiently is important for writing robust tests. Jest provides built-in mechanisms to verify that your code throws exceptions as expected, which helps catch bugs early and ensures your error handling logic behaves correctly.
At its core, Jest’s toThrow matcher is designed to test if a function throws an error when executed. It’s simple but powerful, so that you can assert not only that an error was thrown but also to match the error message or type.
Consider a function that throws an error under certain conditions:
function parseJSON(input) {
if (typeof input !== 'string') {
throw new TypeError('Input must be a string');
}
return JSON.parse(input);
}
You can test this with Jest’s toThrow matcher by wrapping the call inside a function, like so:
test('parseJSON throws TypeError when input is not a string', () => {
expect(() => parseJSON(42)).toThrow(TypeError);
expect(() => parseJSON(42)).toThrow('Input must be a string');
});
This pattern—passing a function to expect—is important because Jest needs to call the function itself to catch the error. Passing the function’s result directly won’t work, as the error would be thrown before Jest can intercept it.
Jest also allows for more nuanced error matching. You can use regular expressions to check error messages, which is handy when the exact message might vary slightly:
expect(() => parseJSON(null)).toThrow(/must be a string/);
Understanding this mechanism sets the foundation for testing more complex scenarios, especially when dealing with asynchronous code or promise rejections, where error handling becomes less simpler.
It’s worth noting that toThrow only works on functions that throw synchronously. For asynchronous code, Jest provides different tools to handle errors, which we’ll explore next.
MNN Portable Monitor 15.6inch FHD 1080P 60Hz USB C HDMI Gaming Ultra-Slim IPS Display w/Smart Cover & Speakers,HDR Plug&Play, External Monitor for Laptop PC Phone Mac (15.6'' 1080P)
$49.99 (as of June 2, 2026 22:39 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)Writing tests for synchronous exceptions
When your function throws different types of errors based on various inputs, it’s often useful to assert the specific error type to ensure your code handles each case appropriately. This helps catch subtle bugs where an unexpected error might be thrown.
function validateUser(user) {
if (!user.name) {
throw new Error('Name is required');
}
if (typeof user.age !== 'number') {
throw new TypeError('Age must be a number');
}
}
Testing the above function involves checking for multiple error types:
test('validateUser throws generic Error if name is missing', () => {
expect(() => validateUser({ age: 30 })).toThrow(Error);
expect(() => validateUser({ age: 30 })).toThrow('Name is required');
});
test('validateUser throws TypeError if age is not a number', () => {
expect(() => validateUser({ name: 'Alice', age: 'thirty' })).toThrow(TypeError);
expect(() => validateUser({ name: 'Alice', age: 'thirty' })).toThrow('Age must be a number');
});
It’s important to wrap the call in a function each time. If you call the function directly inside expect, the error will occur immediately and Jest won’t be able to catch it for assertion.
Sometimes, you want to test that a function does not throw an error for valid inputs. Jest provides the not modifier combined with toThrow for this purpose:
test('validateUser does not throw for valid user', () => {
expect(() => validateUser({ name: 'Bob', age: 25 })).not.toThrow();
});
Using not.toThrow() ensures that your validation logic permits correct data without raising exceptions.
In cases where your error messages include dynamic data, regular expressions become especially useful. For example:
function divide(a, b) {
if (b === 0) {
throw new Error(Cannot divide ${a} by zero);
}
return a / b;
}
Testing the dynamic error message with a regex allows flexibility:
test('divide throws error with dynamic message on division by zero', () => {
expect(() => divide(10, 0)).toThrow(/Cannot divide d+ by zero/);
});
This approach avoids brittle tests that break if the exact message changes slightly but still verifies the error context.
Finally, when testing functions that throw custom error classes, you can assert the error instance using toThrow with the constructor. For example:
class AuthenticationError extends Error {}
function authenticate(user) {
if (!user.isAuthenticated) {
throw new AuthenticationError('User not authenticated');
}
}
You verify that the correct error type is thrown:
test('authenticate throws AuthenticationError when user is not authenticated', () => {
expect(() => authenticate({ isAuthenticated: false })).toThrow(AuthenticationError);
});
This ensures your error handling logic remains precise, helping downstream code react appropriately to specific failure modes. Understanding synchronous error testing is foundational before tackling asynchronous error handling, where promises and async/await introduce new patterns for catching exceptions.
Testing asynchronous errors and promise rejections
When dealing with asynchronous operations, such as API calls or timers, error handling becomes more complex. Jest offers mechanisms to test these scenarios effectively, particularly when working with promises. The key is to ensure that you are correctly handling promise rejections and using the right assertions to verify that errors are thrown as expected.
To test a function that returns a promise and may reject, you can use Jest’s rejects matcher. This matcher allows you to assert that a promise will reject with a specific error. Here’s an example of a function that simulates an asynchronous operation:
function fetchData(url) {
return new Promise((resolve, reject) => {
if (!url) {
reject(new Error('URL is required'));
} else {
resolve({ data: 'some data' });
}
});
}
You can write a test to ensure that the promise rejects when no URL is provided:
test('fetchData rejects with error if URL is not provided', () => {
return expect(fetchData()).rejects.toThrow('URL is required');
});
In this case, the test checks that when the promise returned by fetchData is executed without a URL, it rejects with the specified error message.
Jest also allows you to assert the type of the error thrown by the rejected promise. This can be particularly useful when you want to ensure that your error handling logic is robust:
test('fetchData rejects with an instance of Error', () => {
return expect(fetchData()).rejects.toThrow(Error);
});
For asynchronous functions using async/await, you can write tests that look cleaner and more simpler. Here’s how you might test the same function using async/await:
test('fetchData throws error if URL is not provided using async/await', async () => {
await expect(fetchData()).rejects.toThrow('URL is required');
});
This approach allows for a more readable syntax, particularly when dealing with multiple asynchronous operations in a single test.
When testing a function that resolves successfully, you can also verify that the returned data is correct:
test('fetchData resolves with data when URL is provided', async () => {
const response = await fetchData('https://api.example.com/data');
expect(response).toEqual({ data: 'some data' });
});
This ensures that your function not only handles errors correctly but also returns the expected results when called with valid inputs.
Additionally, if you’re working with multiple asynchronous functions, you might want to test that they work together correctly. For example, if one function depends on the output of another, you can chain them in your tests:
async function processData(url) {
const response = await fetchData(url);
return response.data;
}
test('processData fetches and processes data correctly', async () => {
const data = await processData('https://api.example.com/data');
expect(data).toBe('some data');
});
This kind of testing is essential in ensuring that your application behaves as expected under various conditions, especially when dealing with asynchronous behavior.
