Catching Errors Correctly: How to Test Exceptions in JUnit in 2025

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: "Why does my test fail when it should pass?"

\n\n

Have you ever written a unit test that was supposed to catch an exception, but instead it failed with a red error? Sound familiar? The question "How do you assert that a certain exception is thrown in JUnit tests?" on Stack Overflow has gathered thousands of views and hundreds of comments. And this is no coincidence: testing exceptions is one of those topics where even experienced developers sometimes stumble.

\n\n

In 2025, when code quality has become a critical factor (especially in microservices and AI pipelines), the ability to correctly check exceptions is not just "good practice," but a core competency. In this article, we will break down all current ways to assert exceptions in JUnit 5, show the pitfalls, and provide a checklist of best practices. Let's go!

\n\n

Why is it important to test exceptions?

\n\n

Imagine: you are writing a method divide(int a, int b). If b == 0, the method should throw an ArithmeticException. Without a test for this behavior, you risk that a month later another developer (or yourself) accidentally removes the check, and the application crashes in production with an unclear error.

\n\n

Exception tests are your insurance. They document the expected behavior of the system in edge cases. And JUnit 5 provides several elegant tools for this.

\n\n

Method 1: assertThrows — the king of modern testing

\n\n

Starting with JUnit 5 (and its package org.junit.jupiter.api), the assertThrows method has become the de facto standard. Why? Because it is concise, readable, and functional.

\n\n
import static org.junit.jupiter.api.Assertions.assertThrows;\n\n@Test\nvoid whenDivideByZero_thenArithmeticException() {\n    assertThrows(ArithmeticException.class, \n        () -> calculator.divide(10, 0));\n}\n
\n\n

What's happening here? assertThrows takes the expected exception class and a lambda with the code that should throw it. If the exception is thrown — the test is green. If not — it's red. Simple and clear.

\n\n

Extended version: checking the exception message

\n\n

Sometimes it's not enough to just check the exception type — you need to ensure the message contains the correct text. assertThrows returns the exception object, and you can check its properties:

\n\n
import static org.junit.jupiter.api.Assertions.*;\n\n@Test\nvoid whenInvalidEmail_thenExceptionWithMessage() {\n    IllegalArgumentException exception = assertThrows(\n        IllegalArgumentException.class,\n        () -> userService.register("invalid-email")\n    );\n    \n    assertTrue(exception.getMessage().contains("Invalid email"));\n}\n
\n\n

This approach is especially useful when testing custom exceptions with fields (e.g., an error code).

\n\n

Method 2: The good old expected (JUnit 4 legacy)

\n\n

If you are working with JUnit 4 or maintaining legacy code, you have likely encountered the expected attribute in the @Test annotation:

\n\n
@Test(expected = ArithmeticException.class)\npublic void whenDivideByZero_thenException() {\n    calculator.divide(10, 0);\n}\n
\n\n

Downside: this method does not allow you to check the exception message or its properties. Additionally, if an exception is thrown earlier than expected (e.g., in an initialization line), the test might pass falsely. In 2025, we recommend avoiding this approach in new code.

\n\n

Method 3: Try-catch with a flag (for control enthusiasts)

\n\n

Before the advent of JUnit 5, many used a construct with try-catch and a boolean flag. It looks cumbersome but gives full control:

\n\n
@Test\nvoid whenDivideByZero_thenExceptionCaught() {\n    boolean thrown = false;\n    try {\n        calculator.divide(10, 0);\n    } catch (Arithmeti                

Blogs

Book Recommendations