JUnit’s assertThrows() method runs a piece of code and passes only when that code throws the expected exception type or one of its subclasses. It returns the thrown exception, so the same test can check the exception message and the cause. The method exists in JUnit 5 and JUnit 6 with the same signature, so the code in this article works on both.
We assert an exception whenever a method must reject bad input or a broken precondition, for example a parser that gets “One” instead of a number, or a service that gets a null argument. A test that only checks the happy path never finds out that the method accepts invalid data.
The following example shows the three exception assertions in JUnit, each line with its result as a comment.
// 1. assertThrows() passes for the type or a subclass, and returns the exception
NumberFormatException thrown = assertThrows(NumberFormatException.class, () -> Integer.parseInt("One"));
String message = thrown.getMessage(); // For input string: "One"
// 2. assertThrowsExactly() passes only for the exact type
NumberFormatException exact = assertThrowsExactly(NumberFormatException.class, () -> Integer.parseInt("One")); // passes, same class
// 3. assertDoesNotThrow() passes when nothing is thrown, and returns the lambda's result
int number = assertDoesNotThrow(() -> Integer.parseInt("42")); // 42
Notice that each method takes the code under test as a lambda, so JUnit decides when to run it and can catch what it throws. All three are static methods of org.junit.jupiter.api.Assertions.
The three methods differ in what they accept and what they return, and the difference matters as soon as our code throws a subclass.
| Method | Passes when the code | Returns |
|---|---|---|
| assertThrows() | throws the expected type or a subclass of it | the thrown exception |
| assertThrowsExactly() | throws the expected type and not a subclass (since JUnit 5.8) | the thrown exception |
| assertDoesNotThrow() | throws nothing | the lambda’s return value, if it has one |
After the syntax, we check messages and causes, test checked exceptions and parameterized inputs, compare AssertJ’s assertThatThrownBy(), and migrate JUnit 4’s @Test(expected) and ExpectedException rule.
1. JUnit assertThrows() API
The assertThrows() method verifies that a particular type of exception (or any of its subclasses) is thrown when a code block is executed. If the check passes, the method returns the exception object, typed as the class we asked for, so no cast is needed.
The examples in this article use JUnit 6.1.3, AssertJ 3.27.7 and Java 21, built with Maven. JUnit 6 needs Java 17 or later and uses one version number for all its modules. The test code also compiles and passes on JUnit 5.8 or later, because assertThrows() and its siblings did not change between the two major versions. The complete tests are in the Junit5Examples repository on GitHub.
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>6.1.3</version>
<scope>test</scope>
</dependency>
The junit-jupiter artifact brings the API, the params module and the engine. On JUnit 5, we keep the same code and set the version to a 5.x release, as shown in JUnit 6 Maven dependency.
1.1. Method Syntax
The assertThrows() method asserts that execution of the supplied executable block or lambda expression throws an exception of the expectedType. It is an overloaded method with three versions.
static <T extends Throwable> T assertThrows(Class<T> expectedType, Executable executable)
static <T extends Throwable> T assertThrows(Class<T> expectedType, Executable executable, String message)
static <T extends Throwable> T assertThrows(Class<T> expectedType, Executable executable, Supplier<String> messageSupplier)
Each parameter has one job.
- The expectedType is the exception class that the test code must throw.
- The executable is the code under test, written as a lambda or a method reference.
- The message is printed at the start of the failure message when the assertion fails.
- The messageSupplier builds that message only when the assertion fails, which helps when the text is expensive to build.
The Executable interface has a single method, void execute() throws Throwable. Because it declares Throwable, the lambda can call methods that throw checked exceptions without a try-catch block, as we will see in section 1.4.
1.2. Matches Specified Exception or Its Child Exception
The assertThrows() method fails in two cases.
- The executable block throws no exception.
- The executable block throws an exception that is neither the expected class nor a subclass of it.
For example, “1” is a valid number, so Integer.parseInt(“1”) throws nothing and the following assertion fails.
NumberFormatException thrown = assertThrows(NumberFormatException.class, () -> Integer.parseInt("1"),
"NumberFormatException error was expected");
JUnit puts our custom message first, followed by the reason.
org.opentest4j.AssertionFailedError: NumberFormatException error was expected ==> Expected java.lang.NumberFormatException to be thrown, but nothing was thrown.
The IDE shows the same message in its test runner.

When a different exception is thrown, the failure message names both classes, and JUnit attaches the real exception as the cause. So the stack trace of the unexpected exception still shows up in the test report.
org.opentest4j.AssertionFailedError: Unexpected exception type thrown, expected: <java.lang.NullPointerException> but was: <java.lang.NumberFormatException>
Caused by: java.lang.NumberFormatException: For input string: "One"
The assertThrows() method passes if the code block throws an exception of the specified type or a subtype. For example, if we expect IllegalArgumentException and the code throws NumberFormatException, the test also passes, because NumberFormatException extends IllegalArgumentException.

Integer.parseInt(“One”) throws NumberFormatException because “One” is not a numeric string. So an assertion that expects NumberFormatException passes, and so does one that expects its parent class, IllegalArgumentException.
NumberFormatException thrown = assertThrows(NumberFormatException.class, () -> {
int number = Integer.parseInt("One");
}, "NumberFormatException was expected");
String message = thrown.getMessage(); // For input string: "One"
IllegalArgumentException parent = assertThrows(IllegalArgumentException.class, () -> {
int number = Integer.parseInt("One");
}); // passes, NumberFormatException is a subclass
Note that if we pass Exception.class as the expected exception type, any exception thrown from the executable block makes the assertion pass, since Exception is the super-type for all checked and unchecked exceptions. Expect the most specific type the method documents.
1.3. Checking the Exception Message and Cause
The exception type alone often proves little. A method can throw IllegalArgumentException for five different reasons, and the test should confirm that it failed for the reason we are testing. Since assertThrows() returns the exception, we check getMessage() and getCause() on the returned object with the usual JUnit assertions.
The following example is a small Playlist class from a music app. Its add(String title) method throws NullPointerException for a null title and IllegalArgumentException for a blank one. Its static parseDuration(String text) method turns “3:45” into 225 seconds, and when a part is not a number, it wraps the NumberFormatException in an IllegalArgumentException.
Playlist playlist = new Playlist();
IllegalArgumentException blank = assertThrows(IllegalArgumentException.class, () -> playlist.add(" "));
NullPointerException missing = assertThrows(NullPointerException.class, () -> playlist.add(null));
String blankMessage = blank.getMessage(); // title must not be blank
String missingMessage = missing.getMessage(); // title must not be null
boolean mentionsBlank = blank.getMessage().contains("blank"); // true
We compare the full message with assertEquals() when the text is fixed. When the message contains a value that changes, such as a timestamp, contains() on the important part keeps the test stable.
For the cause, assertInstanceOf() checks the type and returns the cause already cast, so we can read its message on the next line.
IllegalArgumentException invalid = assertThrows(IllegalArgumentException.class,
() -> Playlist.parseDuration("x:45"));
NumberFormatException cause = assertInstanceOf(NumberFormatException.class, invalid.getCause());
String message = invalid.getMessage(); // Invalid duration: x:45
String causeMessage = cause.getMessage(); // For input string: "x"
If getCause() returns null or another type, assertInstanceOf() fails the test, so a missing cause never slips through.
1.4. Testing Checked Exceptions
A checked exception needs no extra code in the test, because Executable.execute() declares throws Throwable. We pass the method call as it is, and JUnit catches the exception. The difference between the two kinds is covered in checked vs unchecked exceptions.
In the example, Playlist.load(Path file) reads song titles from a file and wraps any IOException in a checked PlaylistException. When the file does not exist, the cause is a NoSuchFileException.
Path missingFile = Path.of("missing-playlist.txt");
PlaylistException failed = assertThrows(PlaylistException.class, () -> Playlist.load(missingFile));
NoSuchFileException cause = assertInstanceOf(NoSuchFileException.class, failed.getCause());
String message = failed.getMessage(); // Cannot load playlist missing-playlist.txt
String causeMessage = cause.getMessage(); // missing-playlist.txt
The test method itself needs no throws clause. A throws clause on a test method is not an expectation either, so it never makes a test pass.
1.5. Keeping Only the Throwing Call Inside the Lambda
The lambda stops at the first exception from any of its lines, and assertThrows() passes if that exception matches. If setup code inside the lambda throws the same type, the test passes for the wrong reason.
Say a test for our playlist adds a song first and expects the second call, add(“”), to fail. If both calls sit in the lambda and the first one throws IllegalArgumentException because of a typo in the test data, the test still passes, and the real check never runs. So we keep the setup outside and put only the call under test inside.
Playlist playlist = new Playlist();
int added = playlist.add("Hey Jude"); // 1
IllegalArgumentException blank = assertThrows(IllegalArgumentException.class, () -> playlist.add(""));
String message = blank.getMessage(); // title must not be blank
int size = playlist.songs().size(); // 1
The last line shows a second benefit. After the exception, we can check that the object stayed in a valid state, which is a part of the contract that tests often skip.
2. JUnit assertThrowsExactly() API
The assertThrowsExactly() method is similar to assertThrows(), but it verifies that only the exact type of exception specified is thrown, and not any subclass. It has the same three overloads and also returns the exception. JUnit added it in version 5.8.
We use it when a subclass would mean a different failure. For example, parseDuration() throws a plain IllegalArgumentException for a wrong format, and a test that expects that exact class fails if the method ever lets a raw NumberFormatException escape.
IllegalArgumentException wrongFormat = assertThrowsExactly(IllegalArgumentException.class,
() -> Playlist.parseDuration("3"));
String message = wrongFormat.getMessage(); // Invalid duration: 3
Let’s modify the previous test with the parent type. It passed with the assertThrows() method, but when we match the exact exception type, it fails, because Integer.parseInt(“One”) throws the subclass NumberFormatException.
IllegalArgumentException thrown = assertThrowsExactly(IllegalArgumentException.class,
() -> Integer.parseInt("One"));
The message has the same format as a type mismatch in assertThrows().
org.opentest4j.AssertionFailedError: Unexpected exception type thrown, expected: <java.lang.IllegalArgumentException> but was: <java.lang.NumberFormatException>
Caused by: java.lang.NumberFormatException: For input string: "One"
The class hierarchy decides the result. The diagram compares both methods for each expected type in a test of Integer.parseInt(“One”).

3. JUnit assertDoesNotThrow()
The assertDoesNotThrow() method verifies that the block of code throws no exception. We use it for the valid inputs next to the invalid ones, such as boundary values that sit right next to a rejected value, so a reader of the test sees both sides of the rule.
The method has two forms. With an Executable, it returns nothing. With a ThrowingSupplier, which is a lambda that returns a value, it returns that value, so we can assert on the result in the same test.
Playlist playlist = new Playlist();
int seconds = assertDoesNotThrow(() -> Playlist.parseDuration("3:45")); // 225
int size = assertDoesNotThrow(() -> playlist.add("Yesterday"), "valid titles are accepted"); // 1
Without assertDoesNotThrow(), an unexpected exception also fails the test, because JUnit marks any uncaught exception as a failure. The difference is the report. The assertDoesNotThrow() method fails with a clear message and keeps the original exception and its cause chain.
org.opentest4j.AssertionFailedError: Unexpected exception thrown: java.lang.IllegalArgumentException: Invalid duration: x:45
Caused by: java.lang.IllegalArgumentException: Invalid duration: x:45
Caused by: java.lang.NumberFormatException: For input string: "x"
4. Testing Exceptions in Parameterized Tests
When several inputs must fail the same way, a parameterized test runs one assertThrows() per input, and the report lists every input as its own test. We get one failing line per bad input instead of a test that stops at the first broken value.
The following test feeds five invalid durations to parseDuration(), namely a missing colon, too many parts, a letter, 60 seconds and negative minutes.
@ParameterizedTest
@ValueSource(strings = {"3", "3:4:5", "x:45", "3:60", "-1:30"})
void invalidDurationsAreRejected(String text) {
IllegalArgumentException invalid = assertThrows(IllegalArgumentException.class,
() -> Playlist.parseDuration(text));
assertEquals("Invalid duration: " + text, invalid.getMessage());
}
If different inputs throw different exceptions, we pass the expected class as an argument. A @MethodSource provider returns the input, the exception class and the message for each case.
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("badDurations")
void eachBadInputThrowsItsOwnException(String text, Class<? extends Exception> expectedType, String expectedMessage) {
Exception thrown = assertThrowsExactly(expectedType, () -> Playlist.parseDuration(text));
assertEquals(expectedMessage, thrown.getMessage());
}
static Stream<Arguments> badDurations() {
return Stream.of(
Arguments.of(null, NullPointerException.class, "duration must not be null"),
Arguments.of("3", IllegalArgumentException.class, "Invalid duration: 3"),
Arguments.of("x:45", IllegalArgumentException.class, "Invalid duration: x:45"));
}
We use assertThrowsExactly() in this test on purpose. With assertThrows() and a broad type in the table, a row could pass with the wrong subclass.
The valid inputs go into their own test with assertDoesNotThrow(), which returns the parsed value for the comparison.
@ParameterizedTest
@CsvSource({"3:45, 225", "0:59, 59", "10:00, 600"})
void validDurationsAreParsed(String text, int expectedSeconds) {
int seconds = assertDoesNotThrow(() -> Playlist.parseDuration(text));
assertEquals(expectedSeconds, seconds);
}
5. AssertJ assertThatThrownBy() as an Alternative
AssertJ is an assertion library with a fluent API that many teams use next to JUnit. Its assertThatThrownBy() method catches the exception and chains the type, message and cause checks in one statement, so the test reads as one sentence. We cover the library in more depth in the AssertJ tutorial.
Playlist playlist = new Playlist();
assertThatThrownBy(() -> Playlist.parseDuration("x:45"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Invalid duration: x:45")
.hasCauseInstanceOf(NumberFormatException.class)
.hasRootCauseMessage("For input string: \"x\"");
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> playlist.add(" "))
.withMessage("title must not be blank");
IllegalArgumentException invalid = catchThrowableOfType(IllegalArgumentException.class,
() -> Playlist.parseDuration("x:45")); // returns the exception, like assertThrows()
assertThatCode(() -> Playlist.parseDuration("3:45")).doesNotThrowAnyException();
AssertJ also prints both values when a message check fails, so a typo in the expected text shows up at once.
org.opentest4j.AssertionFailedError:
Expecting message to be:
"Invalid duration: x45"
but was:
"Invalid duration: x:45"
The two libraries do the same job, so the choice depends on the project. The JUnit methods need no extra dependency and are enough for most tests, whereas AssertJ needs assertj-core and pays off when a test checks messages, causes and root causes together.
| Check | JUnit 5 and JUnit 6 | AssertJ |
|---|---|---|
| Type or subclass | assertThrows(X.class, code) | assertThatThrownBy(code).isInstanceOf(X.class) |
| Exact type | assertThrowsExactly(X.class, code) | assertThatThrownBy(code).isExactlyInstanceOf(X.class) |
| Message | assertEquals(“text”, thrown.getMessage()) | .hasMessage(“text”) or .hasMessageContaining(“part”) |
| Cause | assertInstanceOf(Y.class, thrown.getCause()) | .hasCauseInstanceOf(Y.class), .hasRootCauseMessage(“text”) |
| No exception | assertDoesNotThrow(code) | assertThatCode(code).doesNotThrowAnyException() |
6. Expected Exception @Rule in JUnit 4
In JUnit 4, we can use the @Test(expected = …) attribute or the @Rule annotation along with ExpectedException to assert exceptions. Neither exists in JUnit 5 or JUnit 6, so both must change when we migrate from JUnit 4.
The following class rewrites the previous example in JUnit 4 syntax. Since JUnit 4.13, ExpectedException.none() is deprecated, and the compiler warns about it.
@Rule
public ExpectedException expectedException = ExpectedException.none(); // deprecated since 4.13
@Test(expected = NumberFormatException.class)
public void expectedAttribute() {
int number = Integer.parseInt("One");
}
@Test
public void testExpectedException() {
expectedException.expect(NumberFormatException.class);
expectedException.expectMessage("For input string: \"One\"");
int number = Integer.parseInt("One");
}
@Test
public void testExpectedExceptionWithParentType() {
expectedException.expect(IllegalArgumentException.class);
int number = Integer.parseInt("One");
}
The @Test(expected) attribute has the same weakness as a long lambda in section 1.5. Any line of the test method can throw the exception, and the test passes. When nothing is thrown, JUnit 4 fails with java.lang.AssertionError: Expected exception: java.lang.NumberFormatException.
Each JUnit 4 construct has a direct replacement.
| JUnit 4 | JUnit 5 and JUnit 6 |
|---|---|
| @Test(expected = NumberFormatException.class) | assertThrows(NumberFormatException.class, () -> Integer.parseInt(“One”)) |
| expectedException.expect(X.class) | X thrown = assertThrows(X.class, code) |
| expectedException.expectMessage(“One”), which matches a substring | assertTrue(thrown.getMessage().contains(“One”)), or assertEquals() for the full text |
| expectedException.expectCause(instanceOf(Y.class)) | assertInstanceOf(Y.class, thrown.getCause()) |
| Assert.assertThrows(“message”, X.class, code) (JUnit 4.13) | Assertions.assertThrows(X.class, code, “message”), with the message last |
| No equivalent | assertThrowsExactly() and assertDoesNotThrow() |
JUnit 4.13 already has Assert.assertThrows(), which also returns the exception. Migrating to it first keeps the tests on JUnit 4 while removing the rule, and the later switch to JUnit Jupiter only moves the message argument to the end.
For large legacy suites, the junit-jupiter-migrationsupport module runs unchanged ExpectedException rules in JUnit Jupiter tests with the class-level @EnableRuleMigrationSupport annotation. JUnit 6.0 deprecated the module, although it is still published for 6.1.3, and the JUnit team calls it limited rule support for the migration period, so we use it only while the tests move to assertThrows().
7. Expected Exception FAQs
Most questions about assertThrows() come from its subclass matching and from the move between JUnit versions.
7.1. Is assertThrows() the Same in JUnit 5 and JUnit 6?
Yes. The method has the same package, org.junit.jupiter.api.Assertions, and the same overloads in both versions, and it returns the exception in both. JUnit 6 raises the minimum Java version to 17, so a project on Java 11 stays on JUnit 5, but its exception tests look the same.
7.2. Why Does assertThrows() Pass When a Different Exception Is Thrown?
The test passes because the thrown exception is a subclass of the expected type. For example, assertThrows(IllegalArgumentException.class, …) accepts NumberFormatException, and assertThrows(Exception.class, …) accepts almost anything. Use assertThrowsExactly() or a more specific type when the subclass matters.
7.3. Does assertThrows() Work With Checked Exceptions?
Yes. The lambda is an Executable, whose execute() method declares throws Throwable, so a call that throws IOException or a custom checked exception compiles without a try-catch block. The returned exception has the expected checked type, as the PlaylistException test in section 1.4 shows.
8. Conclusion
To assert an exception in JUnit, we call assertThrows() with the expected type and a lambda that holds only the call under test. The method returns the exception, so the message and the cause get their own assertions, and assertInstanceOf() returns the cause already cast.
The assertThrowsExactly() method rejects subclasses, which helps when a subclass means a different bug. The assertDoesNotThrow() method covers the valid inputs and returns the lambda’s value. For many inputs, a parameterized test gives one report line per value.
AssertJ’s assertThatThrownBy() reads better when a test checks several details at once. JUnit 4’s @Test(expected) and ExpectedException rule map one to one to assertThrows(), and the same code runs on JUnit 5 and the latest JUnit 6 release.
9. References
- JUnit User Guide, Exception Handling
- Assertions Javadoc (JUnit 6)
- JUnit User Guide, Migrating from JUnit 4
- JUnit User Guide, Parameterized Tests
- ExpectedException Javadoc (JUnit 4)
- AssertJ documentation
Happy Learning !!
Hi, nice article! There’s only one error in the first table with the overview of the JUnit 5 assertion methods, “shouldNotThrowException” does not exist.
Please refer to official docs.
I agree with Ralf. Method “shouldNotThrowException” does not exist.
Most likely it is misprint, here should be “assertDoesNotThrow()” instead of “shouldNotThrowException”.
You are right.