What is the difference between assertThrows and assertThrowsExactly in JUnit 5?
answer
- assertThrows = type OR subtype (is-a)
- assertThrowsExactly = exact runtime class only
- Subtype passes one, fails the other
- Default to assertThrows; exact is brittle
- Both return the caught exception
basics
~20 sassertThrows passes if the code throws the expected type or any subclass of it. assertThrowsExactly passes only if the thrown exception is exactly that class — a subclass fails. Use exactly when the precise type matters.
solid answer
~50 sBoth run an executable and assert it throws, returning the caught exception. They differ in type matching. assertThrows uses an is-a (assignability) check: it passes when the thrown exception is the expected type or any subtype. assertThrowsExactly requires an exact class match — the thrown exception's runtime class must equal the expected class, so a subtype fails. For example, if a method throws a custom CustomIllegalArg extends IllegalArgumentException, assertThrows(IllegalArgumentException.class, ...) passes, but assertThrowsExactly(IllegalArgumentException.class, ...) fails because the actual class is the subtype. Prefer assertThrows by default — it is more robust to refactors that introduce more specific subtypes. Reach for assertThrowsExactly only when the exact type is part of the contract you are pinning, e.g. ensuring a generic RuntimeException is NOT silently replaced by a subclass, or asserting a framework returns the base type rather than a narrower one.
go deeper
Knows there are two methods and that 'exactly' is stricter, even if fuzzy on the hierarchy detail.
Clearly states the is-a vs exact-class distinction with a subtype example and knows assertThrowsExactly arrived in 5.8.
Explains the robustness trade-off — assertThrows survives subtype refactors, exactly pins a contract — and picks deliberately per case.
Establishes team guidance: default to assertThrows, reserve exactly for contract-pinning public APIs, and ties it to exception-design and API-stability concerns.
## Background: exceptions form a hierarchy In Java, exceptions are **classes** arranged in an **inheritance tree**. For instance `NumberFormatException` *extends* `IllegalArgumentException`, which *extends* `RuntimeException`, which *extends* `Exception`, which *extends* `Throwable`. A `NumberFormatException` object therefore **is-a** `IllegalArgumentException` too (the *Liskov* is-a relationship). This matters for how an exception assertion decides whether the thrown type 'matches'. ## assertThrows — assignable (subtype) match `assertThrows(ExpectedType.class, executable)` passes when the thrown exception **is an instance of** `ExpectedType` — meaning its class is `ExpectedType` **or any subclass**. Internally it does an `expectedType.isInstance(actual)` style check. So: ``` assertThrows(IllegalArgumentException.class, () -> { throw new NumberFormatException(); }); // PASSES ``` because `NumberFormatException` is a subtype of `IllegalArgumentException`. ## assertThrowsExactly — exact-class match `assertThrowsExactly(ExpectedType.class, executable)` (added in JUnit 5.8) passes **only** when the thrown exception's **runtime class is exactly** `ExpectedType`. A subtype does **not** match: ``` assertThrowsExactly(IllegalArgumentException.class, () -> { throw new NumberFormatException(); }); // FAILS assertThrowsExactly(NumberFormatException.class, () -> { throw new NumberFormatException(); }); // PASSES ``` The check is effectively `actual.getClass() == expectedType`. ## Both return the exception Like `assertThrows`, `assertThrowsExactly` **returns the caught exception**, so you can continue asserting on its message, cause, or fields. ## Which to use? - **Default to `assertThrows`.** It is *robust to refactoring*: if someone later replaces a thrown `IllegalArgumentException` with a more specific subtype, the test still passes — which is usually the right behavior, since callers catching the base type are unaffected. - **Use `assertThrowsExactly`** when the **exact type is part of the contract**. Examples: pinning that a public API throws the precise documented type and not a leaking internal subclass; verifying a generic `RuntimeException` is thrown and was *not* silently narrowed; regression-locking a specific class. ## Common pitfall Using `assertThrowsExactly` by habit makes tests **brittle**: an innocuous refactor that throws a more descriptive subclass breaks the test even though behavior improved. Choose `exactly` deliberately, not reflexively. ## Summary Same mechanics (run executable, return exception); the only difference is the type predicate — `assertThrows` = is-a (type or subtype), `assertThrowsExactly` = exact class only.
- A method throws NumberFormatException (a subclass of IllegalArgumentException). Which call passes: assertThrows(IllegalArgumentException.class, ...) or assertThrowsExactly(IllegalArgumentException.class, ...)?assertThrows passes (subtype satisfies the is-a check); assertThrowsExactly fails because the exact runtime class is NumberFormatException, not IllegalArgumentException.
- When would assertThrowsExactly be the right choice?When the precise exception class is part of the contract — e.g. ensuring a public API throws exactly the documented type and not a leaking internal subclass, or pinning that it is the base type rather than a narrower one.
saying these in an interview costs you the question
- Claiming assertThrows requires an exact type match — it accepts subtypes.
- Reaching for assertThrowsExactly by default, making tests brittle to subtype refactors.
- Thinking the two differ in what they return — both return the exception; only the type predicate differs.
- Believing a superclass of the thrown type would match assertThrows — it is the other direction; the thrown type must be the expected type or below it.