skip to content

JUnit 5 provides TestExecutionExceptionHandler as an extension callback. What can it do with a failing test, what are its limits, and where would you use it responsibly?

level: seniorimportance: should knowfreq 30%

answer

  1. handleTestExecutionException(context, throwable)
  2. rethrow / translate / swallow (returning = pass)
  3. chained across handlers, first to return normally wins
  4. test body only — lifecycle needs LifecycleMethodExecutionExceptionHandler
  5. TestWatcher observes, this one can change the verdict

basics

~20 s

It intercepts a throwable from a @Test method body and may swallow it (return normally, test passes), rethrow it, or rethrow a different one. It does not see exceptions from @BeforeEach/@AfterAll — those need LifecycleMethodExecutionExceptionHandler.

solid answer

~50 s

`TestExecutionExceptionHandler` has one method, `handleTestExecutionException(ExtensionContext context, Throwable throwable)`. It is invoked when the `@Test` method body throws, sitting between the test invocation and `AfterTestExecutionCallback`. Three legitimate outcomes: - **Rethrow** the same throwable — pass it along, having done something useful such as capturing a screenshot, dumping database state or logging diagnostics. - **Rethrow a different throwable** — translate a low-level exception into a domain-specific failure with a better message. - **Return normally** — the exception is swallowed and the test passes. Reserve this for narrow, explicitly opted-in cases (for example tolerating a specific known infrastructure exception), never as a blanket "ignore failures". Several registered handlers form a chain: each sees whatever the previous one rethrew, and the first to return normally ends the failure. Limits: it only covers the test method body. Exceptions thrown by `@BeforeEach`, `@AfterEach`, `@BeforeAll` or `@AfterAll` require `LifecycleMethodExecutionExceptionHandler` instead.

code

java · 11 lines
java
class DiagnosticsExtension implements TestExecutionExceptionHandler {
    @Override
    public void handleTestExecutionException(ExtensionContext ctx, Throwable t) throws Throwable {
        ctx.publishReportEntry("containerLogs", container.getLogs());
        if (t instanceof SQLException sql && "23505".equals(sql.getSQLState())) {
            throw new AssertionError(
                    "fixture inserted a duplicate row: " + sql.getMessage(), t);
        }
        throw t;
    }
}

go deeper

for a junior

Know the signature and that rethrowing keeps the failure while returning normally makes the test pass.

for a middle

Add the translation use case, the chaining behaviour, and the limitation to the test method body.

for a senior

Contrast it with TestWatcher, AfterTestExecutionCallback and InvocationInterceptor, argue for diagnostics-and-rethrow, and cover LifecycleMethodExecutionExceptionHandler for setup failures.

for a principal

Talk about suite trustworthiness: what tolerating a failure costs, how tolerated conditions must remain visible in reports, and governance around retries and flaky-test policy.

## The hook ```java public interface TestExecutionExceptionHandler extends Extension { void handleTestExecutionException(ExtensionContext context, Throwable throwable) throws Throwable; } ``` JUnit calls it when the `@Test` method body completes exceptionally. In the documented callback order it sits immediately after the test method and before `AfterTestExecutionCallback`, so teardown still runs afterwards regardless of what the handler decides. ## The three outcomes **Rethrow the same throwable.** The common, safe pattern: observe the failure, produce a diagnostic artefact, and let it propagate so the test still fails. ```java public void handleTestExecutionException(ExtensionContext ctx, Throwable t) throws Throwable { screenshots.capture(ctx.getRequiredTestMethod().getName()); throw t; } ``` **Rethrow a different throwable.** Translation. A raw `SQLException: unique constraint UQ_1234 violated` becomes `DuplicateCustomerEmail: the fixture already contains [email protected]`. Preserve the original as the cause; discarding it destroys the only evidence. **Return normally — swallow.** The exception disappears and the test is reported as passing. This is powerful and dangerous: a suite that silently swallows failures is worse than no suite. Acceptable only when the tolerated condition is narrow, matched precisely (specific exception type *and* a specific marker on the test), and visible in the report — publish a report entry, and preferably prefer `TestAbortedException` (which marks the test *skipped*, an honest signal) over pretending it passed. ## Chaining When several handlers are registered they form a chain: each receives the throwable the previous one rethrew, so a translating handler and a diagnostics handler compose. The first handler that returns normally ends the chain and the test passes. Because the chain is sensitive to which extension is registered where, avoid designing handlers that depend on a particular neighbour's behaviour. ## What it does not cover The scope is exactly the test method body. Exceptions from user lifecycle methods — `@BeforeAll`, `@BeforeEach`, `@AfterEach`, `@AfterAll` — are **not** routed here. For those, implement `LifecycleMethodExecutionExceptionHandler`, which offers one method per lifecycle phase with the same rethrow/translate/swallow semantics. If your extension exists to capture diagnostics on failure, handling only the test body means you lose exactly the cases where setup blew up, which are often the most confusing ones. It also does not fire for a test that fails for reasons outside its body, such as a timeout enforced by the framework around the invocation, and it is not the place to observe overall outcomes. ## Choosing between this and neighbouring hooks - **`TestWatcher`** — receives `testFailed`, `testSuccessful`, `testAborted`, `testDisabled` notifications *after* the fact. It cannot change the outcome, which makes it the right choice for pure reporting: metrics, flaky-test tracking, links to CI artefacts. Use `TestExecutionExceptionHandler` only when you must influence the outcome or need to act at the precise moment the exception is in flight. - **`AfterTestExecutionCallback`** — can read the execution exception from the extension context and act on it, but likewise cannot suppress it. Good for capturing state right after the body while resources are still open. - **`InvocationInterceptor`** — wraps the whole invocation and can implement retry or run it elsewhere; heavier, and the right tool when you need control over *whether and how* the invocation runs, not just what happens to its exception. ## Responsible uses - **Failure diagnostics.** Screenshot and page source for UI tests, a dump of the container's logs, the contents of a queue, the last N HTTP exchanges from a stub server. Capture, attach or publish, then rethrow. - **Exception translation.** Turn opaque infrastructure errors into messages that name the likely cause and the fix, especially in a shared test framework used by many teams. - **Narrow tolerated conditions.** For example, marking a test aborted (skipped) when a specific external dependency signals it is unavailable, so the suite reports honestly instead of failing red for an environmental reason. Even here, prefer an `ExecutionCondition` that skips *before* running when the unavailability is knowable up front. ## Irresponsible uses to reject in review - A handler that swallows broad types (`Exception`, `AssertionError`) to make a red build green. - Retry logic implemented by catching and re-invoking the test method by reflection — use `InvocationInterceptor` or a retry-capable template if retries are genuinely wanted, and treat retries themselves as a debt with an owner. - Handlers that swallow silently, leaving no trace in the report; if something was tolerated, the report must say so. - Translation that discards the original throwable instead of setting it as the cause. ## Summary Think of it as an interception point where you can add evidence or improve the message on the way out. Changing the verdict is technically available and almost always the wrong choice; when it is right, it should be narrow, explicit and visible.

  • Your extension captures screenshots on failure, but nothing is captured when a test blows up in @BeforeEach. Why, and what do you add?
    TestExecutionExceptionHandler only intercepts throwables from the @Test method body, so a setup failure never reaches it. Implement LifecycleMethodExecutionExceptionHandler as well, which provides handlers for the @BeforeAll, @BeforeEach, @AfterEach and @AfterAll phases with the same rethrow or translate semantics, and share the capture logic between them.
  • A colleague uses this handler to swallow AssertionError for tests tagged "flaky" so the build stays green. What is your objection and what would you propose instead?
    Swallowing reports a failing test as passing, which destroys the suite's signal and hides real regressions behind a tag that nobody revisits. If a test genuinely cannot be trusted, mark it disabled or aborted so the report tells the truth, file an owner and a deadline, and fix or delete it; if the flakiness is environmental and detectable up front, an ExecutionCondition that skips the test is the honest mechanism.

saying these in an interview costs you the question

  • Assuming it also catches exceptions from @BeforeEach or @AfterAll
  • Swallowing broad exception types to keep a build green
  • Translating an exception without preserving the original as the cause
  • Confusing it with TestWatcher, which only observes and cannot change the outcome

context