Introduction: "Why does my test fail when it should pass?"
\n\nHave 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\nIn 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\nWhy is it important to test exceptions?
\n\nImagine: 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.
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\nMethod 1: assertThrows — the king of modern testing
\n\nStarting 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.
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\nWhat'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.
Extended version: checking the exception message
\n\nSometimes 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:
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\nThis approach is especially useful when testing custom exceptions with fields (e.g., an error code).
\n\nMethod 2: The good old expected (JUnit 4 legacy)
\n\nIf you are working with JUnit 4 or maintaining legacy code, you have likely encountered the expected attribute in the @Test annotation:
@Test(expected = ArithmeticException.class)\npublic void whenDivideByZero_thenException() {\n calculator.divide(10, 0);\n}\n\n\nDownside: 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\nMethod 3: Try-catch with a flag (for control enthusiasts)
\n\nBefore the advent of JUnit 5, many used a construct with try-catch and a boolean flag. It looks cumbersome but gives full control:
@Test\nvoid whenDivideByZero_thenExceptionCaught() {\n boolean thrown = false;\n try {\n calculator.divide(10, 0);\n } catch (Arithmeti