How do you assert in JUnit 5 that a piece of code throws a particular exception, and how do you then verify details of that exception such as its message or cause?
answer
- assertThrows(Type.class, Executable) RETURNS the throwable
- nothing thrown -> fail; wrong type -> fail with it as cause
- keep arrangement OUT of the lambda
- assert message contains / typed field / getCause()
- JUnit 5 dropped @Test(expected=...) on purpose
basics
~20 sCall assertThrows(ExpectedType.class, () -> codeUnderTest()). It fails if nothing is thrown or if the thrown type does not match, and on success it returns the thrown exception, typed, so you can go on asserting its message, cause or custom fields.
solid answer
~50 s`assertThrows(IllegalArgumentException.class, () -> service.withdraw(-5))` takes the expected type and an `Executable` lambda. Three outcomes: nothing thrown → the assertion fails with "Expected ... to be thrown, but nothing was thrown"; a non-matching exception → it fails and attaches the unexpected exception as the cause, so you see the real stack trace; a matching exception → the assertion **returns** it. That return value is the point. Capture it and keep asserting: ```java var ex = assertThrows(InsufficientFunds.class, () -> account.withdraw(500)); assertEquals("balance 120, requested 500", ex.getMessage()); assertEquals(ACCOUNT_ID, ex.accountId()); ``` Put only the call under test inside the lambda — arrangement goes above it — otherwise setup failing the same way makes the test pass for the wrong reason. Prefer asserting a typed field or an error code over an exact message string, which is brittle. For a wrapped exception, assert `ex.getCause()`.
code
java · 21 lines@Test
void rejectsOverdraft() {
Account account = new Account("ACC-1", Money.of(120));
InsufficientFunds ex = assertThrows(
InsufficientFunds.class,
() -> account.withdraw(Money.of(500)) // only the call under test
);
assertEquals("ACC-1", ex.accountId()); // typed field: robust
assertTrue(ex.getMessage().contains("500")); // message: contains, not equals
}
@Test
void wrapsDriverFailure() {
RepositoryException ex = assertThrows(
RepositoryException.class, () -> repository.save(order));
SQLException cause = assertInstanceOf(SQLException.class, ex.getCause());
assertEquals("23505", cause.getSQLState());
}go deeper
Show the call, the lambda, and that the return value lets you assert the message. Mention that nothing-thrown is a failure.
Add the mechanics: subtype matching, the unexpected exception attached as cause, message overloads, assertInstanceOf on getCause, and correct lambda scoping.
Focus on assertion strength — narrow types, structured exception fields over message text, cause-chain contracts for wrapping layers, and why setup must stay outside the lambda.
Talk about exception design as an API: typed domain exceptions carrying error codes, a consistent wrapping policy at module boundaries, and tests that pin the contract rather than the prose.
## The assertion `org.junit.jupiter.api.Assertions.assertThrows` has the signature: ```java static <T extends Throwable> T assertThrows(Class<T> expectedType, Executable executable) ``` `Executable` is a functional interface whose single method may throw any `Throwable`, which is why you can pass a lambda that calls a checked-exception-throwing method without adding `throws` to the test. Overloads add a `String` message or a `Supplier<String>` message for extra failure context. ### What it does, step by step 1. Runs the executable. 2. **Nothing thrown** → fails: `Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.` 3. **Something thrown that is an instance of the expected type** (including a subclass) → the assertion succeeds and *returns* that throwable, already typed as `T` so no cast is needed. 4. **Something thrown of a different type** → fails with `Unexpected exception type thrown, expected: <X> but was: <Y>`, and — importantly — the unexpected exception is attached as the cause of the `AssertionFailedError`, so the original stack trace survives into the report. Certain unrecoverable errors, notably `OutOfMemoryError`, are rethrown as-is rather than being converted into an assertion failure. ## Inspecting the returned exception The returned object is the entire value of the pattern. An exception type alone is a weak assertion — plenty of code paths throw `IllegalArgumentException`. Strengthen it: - **Message.** `assertTrue(ex.getMessage().contains("accountId"))` or, when the message is a stable contract, `assertEquals`. Prefer `contains` or a regex for messages that embed variable data. - **Typed fields.** Domain exceptions should carry structured data — an error code, an entity id, a validation field name. Asserting `ex.errorCode()` is far more robust than parsing prose, and it survives copy changes and internationalisation. - **Cause chain.** Frameworks wrap: a JDBC failure surfaces as a `DataAccessException` whose cause is a `SQLException`; reflective invocation wraps in `InvocationTargetException`; a `Future` wraps in `ExecutionException`. Assert `ex.getCause()` — often `assertInstanceOf(SQLException.class, ex.getCause())` — when the wrapping is part of the contract, and `assertSame(original, ex.getCause())` when the requirement is that the *original* instance is propagated. - **Suppressed exceptions.** For try-with-resources scenarios, `ex.getSuppressed()` can be asserted too. `assertInstanceOf(Type.class, value)` (JUnit 5.8+) is the idiomatic way to check a cause's type, because it also returns the value narrowed to that type for further assertions. ## Scoping the lambda Only the call whose failure you are asserting belongs inside the executable: ```java // weak: any of three calls could produce the exception assertThrows(IllegalStateException.class, () -> { var account = repo.load(id); account.freeze(); account.withdraw(10); }); // strong var account = repo.load(id); account.freeze(); assertThrows(IllegalStateException.class, () -> account.withdraw(10)); ``` The first version passes if `repo.load` throws — a setup failure masquerading as verified behaviour. Keep arrangement outside; keep the lambda to a single statement whenever you can. ## Why not try/catch/fail Before lambdas, the idiom was: ```java try { account.withdraw(500); fail("expected InsufficientFunds"); } catch (InsufficientFunds expected) { assertEquals(..., expected.getMessage()); } ``` This works but is verbose, easy to get wrong (forgetting `fail`, or catching so broadly that the `fail` call's own `AssertionError` is swallowed by the catch block), and hides the intent. `assertThrows` is the modern replacement and should be used unconditionally in new code. JUnit 4's `@Test(expected = X.class)` had a worse problem than verbosity: it matched an exception thrown *anywhere* in the test method, including setup, and gave no handle on the exception object. JUnit 5 removed it deliberately — there is no `expected` attribute on Jupiter's `@Test`. ## Common failure modes - **Asserting a supertype.** `assertThrows(Exception.class, ...)` or `RuntimeException.class` passes for a `NullPointerException` caused by a typo. Name the narrowest type that expresses the behaviour, and consider `assertThrowsExactly` when a subtype would be a genuine defect. - **Nothing to throw.** A lambda that merely creates a stream or a builder without a terminal call executes nothing lazily; the assertion then fails with "nothing was thrown" even though the code is broken in the expected way. Make sure the lambda actually triggers evaluation. - **Swallowing inside the lambda.** If the code under test catches and logs, no exception escapes and the assertion fails — which is correct, but candidates often blame the assertion. ## Kotlin and other JVM languages The same API applies; in Kotlin the lambda is a trailing block and the returned exception is a normal value, so `val ex = assertThrows<InsufficientFunds> { account.withdraw(500) }` reads naturally.
- What does assertThrows do if the code throws a different exception than the one expected?The assertion fails with a message naming both types, and JUnit attaches the actually-thrown exception as the *cause* of the `AssertionFailedError`, so its stack trace appears in the report and you can see where the real failure came from. That is a deliberate design choice: an unexpected exception is usually a bug you need to read, not just a mismatch. Truly unrecoverable errors such as `OutOfMemoryError` are rethrown rather than converted.
- Why is asserting on the exact exception message often a bad idea, and what is better?Messages are human-facing prose: they change with wording tweaks, they may embed volatile data such as ids or timestamps, and they can be localised, so an exact-match assertion breaks without any behaviour changing. Prefer asserting a structured field on a domain exception — an error code, the offending field name, the entity id — or use `contains`/a regex on the part of the message that is genuinely contractual. Reserve exact matching for messages that are themselves a published contract, such as API error payloads.
- How does assertThrows differ from JUnit 4's @Test(expected = X.class)?`@Test(expected=...)` matched an exception thrown anywhere in the test method, so a failure in setup or in an unrelated later call satisfied it, and it gave no access to the exception object for further assertions. `assertThrows` scopes the expectation to exactly the code inside the lambda and returns the throwable so you can inspect its message, cause and fields. Jupiter deliberately has no `expected` attribute on `@Test`.
saying these in an interview costs you the question
- Not knowing assertThrows returns the exception, and re-catching it manually to inspect the message.
- Wrapping the whole arrange-act sequence in the lambda, so a setup failure makes the test pass.
- Expecting a broad type such as Exception or RuntimeException, which a stray NullPointerException also satisfies.
- Claiming JUnit 5 still supports @Test(expected = ...).
- Asserting only that some exception was thrown and never checking message, cause or any field.