skip to content

What are the common pitfalls when using assertThrows, including swallowed exceptions, multiple statements in the executable, and using it where exception testing isn't the goal?

level: seniorimportance: nice to knowfreq 33%

answer

  1. Wrap ONE call; do setup outside the lambda
  2. Code after the throw never runs
  3. Don't use assertThrows just to dodge a checked exception
  4. Subtype matches — use assertThrowsExactly to pin exact
  5. getMessage() can be null; assert a substring not the whole thing

basics

~20 s

Don't put many lines in the lambda — an earlier line could throw the expected type and make the test pass for the wrong reason. Wrap only the call you expect to throw. And don't use assertThrows just to silence a checked exception in a test.

solid answer

~50 s

The main pitfalls all stem from the executable being too broad or misused. First, putting multiple statements in the lambda means any of them throwing the expected type satisfies the assertion, so the test can pass for the wrong reason; wrap only the single call under test and do setup outside. Second, assertThrows stops the lambda at the first throw, so statements after the throwing call never run — don't rely on side effects placed after it. Third, people sometimes wrap throwing setup in assertThrows merely to dodge a checked-exception compile error rather than to assert behavior, which hides real failures; use a throws clause on the test method instead. Fourth, assertThrows passes on subtypes, so if you must pin the exact class use assertThrowsExactly. Fifth, don't assert the full message (brittle) or call getMessage().contains on a possibly-null message. Finally, in nested or parameterized code, make sure the exception you catch is the one you intend, not an incidental one from a helper.

code

java · 9 lines
java
// BAD: fat lambda — buildService() throwing IllegalStateException would pass the test
assertThrows(IllegalStateException.class, () -> {
    var svc = buildService();
    svc.process(input);
});

// GOOD: setup outside, wrap only the call under test
var svc = buildService();
assertThrows(IllegalStateException.class, () -> svc.process(input));

go deeper

for a junior

Recognizes that the lambda should hold the failing call and that setup belongs outside.

for a middle

Avoids fat lambdas and post-throw side effects; uses throws on the test method instead of swallowing checked exceptions with assertThrows.

for a senior

Anticipates passing-for-the-wrong-reason cases, picks assertThrows vs assertThrowsExactly deliberately, and writes robust, null-safe message/cause assertions.

for a principal

Codifies these as review standards and lint guidance so the team's exception tests fail only for the intended reason, improving signal across the suite.

## Pitfall 1 — too many statements in the executable `assertThrows` passes if **any** statement inside the lambda throws the expected type. So a fat lambda can pass for the wrong reason: ``` assertThrows(IllegalStateException.class, () -> { var svc = buildService(); // if THIS throws IllegalStateException, test 'passes' svc.process(input); // ...but the line you cared about never ran }); ``` **Fix:** keep arrangement *outside* and wrap only the one call you expect to throw: ``` var svc = buildService(); assertThrows(IllegalStateException.class, () -> svc.process(input)); ``` ## Pitfall 2 — code after the throw never runs The executable executes until the first throw, then control returns to `assertThrows`. Any statement **after** the throwing call is skipped: ``` assertThrows(X.class, () -> { mayThrow(); counter.increment(); // NEVER executes if mayThrow() throws }); ``` Don't put assertions or side effects you depend on after the throwing line. Make those *separate* assertions after `assertThrows` returns. ## Pitfall 3 — using assertThrows to dodge a checked exception A tempting misuse: production setup declares a checked exception, so to avoid a `try/catch` or `throws`, someone wraps it in `assertThrows` they don't actually care about. This **hides real failures** — an unexpected throw there becomes a passing test. **Fix:** add `throws Exception` to the `@Test` method signature (allowed in JUnit) or a narrow `throws`, and reserve `assertThrows` for code you are genuinely asserting throws. ## Pitfall 4 — subtype vs exact-type confusion `assertThrows` matches the expected type **or any subtype**. If your intent is to pin the precise class (e.g. ensure no leaking subclass), use **`assertThrowsExactly`**. Conversely, don't use `assertThrowsExactly` reflexively — it makes tests brittle to subtype refactors. ## Pitfall 5 — brittle or unsafe message assertions Asserting the **entire** message pins wording and breaks on any reword/i18n. And `getMessage()` may be **`null`** (e.g. a bare `NullPointerException`), so `getMessage().contains(...)` can itself throw inside the test. Prefer a **stable substring**, the **cause type**, or **structured fields**, and guard for null. ## Pitfall 6 — catching an incidental exception In nested/parameterized code, a helper or a parameter conversion might throw the same type for an unrelated reason, so the assertion 'passes' on the wrong throw. Keep the lambda minimal and, when in doubt, assert the **message/cause** to confirm it is the expected failure, not a lookalike. ## Why these matter All six reduce to one principle: an exception assertion should fail **only** when the *specific* code under test fails to throw the *intended* exception. Broad lambdas, misuse-as-compile-trick, and brittle message checks all weaken that guarantee — letting tests pass for the wrong reason or break for no real reason. ## Summary Wrap only the throwing call (setup outside), don't depend on post-throw statements, never use assertThrows merely to swallow a checked exception, choose assertThrows vs assertThrowsExactly deliberately, assert stable message fragments/cause types (guarding null), and confirm you caught the *intended* exception.

  • Why is wrapping multiple statements in the executable risky?
    assertThrows passes if any statement throws the expected type, so an earlier setup line throwing it makes the test pass even though the call you cared about never ran or never threw.
  • What is wrong with using assertThrows just to avoid a checked-exception compile error in test setup?
    It turns an unexpected throw in setup into a passing test, hiding real failures. Instead add 'throws Exception' to the @Test method and reserve assertThrows for behavior you actually assert.

saying these in an interview costs you the question

  • Putting setup and the call under test together in one lambda.
  • Relying on statements placed after the throwing call inside the lambda.
  • Using assertThrows as a way to swallow checked exceptions in setup.
  • Calling getMessage().contains(...) without considering a null message.
  • Assuming assertThrows pins the exact class — it accepts subtypes.

context