skip to content

In JUnit 5, what happens to the rest of the run when an exception is thrown inside a @BeforeEach method, and how does that differ from an exception thrown in the test body or in an @AfterEach method?

level: seniorimportance: should knowfreq 28%

answer

  1. @BeforeEach fails ⇒ test skipped-as-failed, @AfterEach still runs
  2. remaining before-hooks skipped
  3. teardown failure fails a passing test
  4. first throwable primary, rest suppressed
  5. TestAbortedException ⇒ aborted, not failed

basics

~20 s

A failing @BeforeEach skips the remaining setup methods and the test body, and the test is reported as failed — but all @AfterEach methods still run. A failure in the test body still runs teardown. If several stages fail, JUnit reports the first as the primary failure with the others attached as suppressed exceptions.

solid answer

~50 s

Jupiter collects throwables across the whole test lifecycle instead of aborting at the first one. - **`@BeforeEach` throws** — remaining `@BeforeEach` methods and the test body are skipped; the test is reported **failed** (not skipped) with that exception. Crucially, **all `@AfterEach` methods still execute**, so cleanup is not lost. - **The test body throws** — the test fails; `@AfterEach` methods still run. - **`@AfterEach` throws** — if the test had already failed, the earlier failure stays primary and the teardown exception is attached as a *suppressed* exception; if the test passed, the teardown failure fails the test. The same applies one level up: a failing `@BeforeAll` fails the class's tests, and `@AfterAll` still runs. Two distinctions matter. A `TestAbortedException` — what a failed assumption throws — marks the test **aborted/skipped**, not failed. And unrecoverable errors such as `OutOfMemoryError` are rethrown immediately rather than collected.

code

java · 23 lines
java
import org.junit.jupiter.api.*;

class ResilientTeardownTest {

    private Connection connection; // may stay null if setup blew up

    @BeforeEach
    void open() {
        connection = Database.connect(); // may throw
        connection.begin();              // may throw after connect succeeded
    }

    @AfterEach
    void close() {
        // runs even when open() threw, so it must be null-safe
        if (connection != null) {
            connection.close();
        }
    }

    @Test
    void query() { /* not executed if open() threw */ }
}

go deeper

for a junior

Know that a failing setup fails the test and that teardown still runs.

for a middle

Cover all three positions, that remaining setup methods are skipped, and that a teardown exception can fail an otherwise passing test.

for a senior

Add the aggregation model — first throwable primary, the rest suppressed — the abort-versus-fail distinction, and the practical rule that teardown must be null-safe because it can run against a half-built fixture.

for a principal

Discuss failure attribution across a suite: how swallowed teardown errors hide resource leaks, why cleanup belongs in extensions that always run, and how reporting choices affect triage time in CI.

## The mechanism: a throwable collector Jupiter does not run the lifecycle as a plain sequence of calls that unwinds on the first exception. Each phase is executed through a collector that captures any throwable and lets execution continue into the phases that must still run — principally teardown. At the end, the collected throwables are combined into a single reported result. That design is what delivers the guarantee people rely on: **cleanup runs even when setup or the test failed**. ## Failure in `@BeforeEach` Suppose a class has two `@BeforeEach` methods, `a()` and `b()`, and `a()` throws. - `b()` is **not** executed. Once setup has failed, running further setup is unsafe and pointless. - The `@Test` method is **not** executed. - All `@AfterEach` methods **are** executed. This is deliberate: an earlier hook — or a partially completed `a()` — may already have acquired resources, so teardown must get its chance. Well-written `@AfterEach` code must therefore be null-safe and tolerant of a half-initialised fixture, because it can run when setup never completed. - The test is reported as **failed** with the exception from `a()`. It is not reported as skipped: a broken fixture is a broken test, and hiding it as "skipped" would let a suite go green while testing nothing. Inherited hooks follow the same rule with the hierarchy ordering: if the superclass `@BeforeEach` fails, the subclass's setup and the test are skipped, and the `@AfterEach` methods of both classes still run. ## Failure in the test body The familiar case. The test fails with that throwable (an `AssertionFailedError` for a failed assertion, or whatever escaped), and `@AfterEach` methods run afterwards. ## Failure in `@AfterEach` Teardown failures are real failures — they are not swallowed. - If nothing had failed yet, the exception from `@AfterEach` **fails the test**, even though the assertions passed. This is how leaked resources, unverified mock interactions or an unclosed temp directory surface. - If the test had already failed, Jupiter keeps the **first** throwable as the primary reported failure and attaches later ones via `Throwable.addSuppressed`. That ordering choice is important for diagnosis: the assertion that actually failed stays at the top of the report, while the cascading teardown noise it caused is retained but demoted. In an IDE or CI report you see the suppressed entries under the main stack trace. - If several `@AfterEach` methods exist, **all** of them run; a failure in one does not prevent the others. ## Class-level hooks The same shape one scope up. A `@BeforeAll` failure means the class's test methods do not run and are reported as failed; `@AfterAll` methods still execute so a partially started shared fixture can be released. An `@AfterAll` failure is reported against the container, which in some CI presentations looks like a class-level error with no owning test — worth knowing when triaging. ## Aborted versus failed Assumptions (`Assumptions.assumeTrue`, `assumeThat`) throw `TestAbortedException`, which the engine treats as **aborted**: the test is reported as skipped rather than failed. Throwing it from a `@BeforeEach` therefore skips the test instead of failing it, which is exactly how you write "this test only applies on Linux" logic in setup. Distinguishing abort from failure is the detail that separates a solid answer from a great one. ## Unrecoverable throwables Jupiter treats a small set of errors — `OutOfMemoryError` most notably — as unrecoverable: they are rethrown immediately rather than collected, because attempting to continue the lifecycle in a JVM that cannot allocate is futile. ## Practical consequences 1. **Write teardown defensively.** `@AfterEach` can run against a fixture that was never fully built. Null-check, or use `Optional`/`if (x != null)` guards. 2. **Don't hide failures in teardown.** Wrapping `@AfterEach` bodies in empty catch blocks to "avoid noisy failures" destroys the signal that resources leaked. 3. **Read suppressed exceptions.** When a failure looks nonsensical, the real cause is often the primary exception with a misleading suppressed cascade, or vice versa. 4. **Prefer extensions for cleanup that must be robust.** Extension callbacks participate in the same collector, and `@TempDir`-style automatic cleanup runs regardless of outcome, which is more reliable than hand-written teardown. ## Answering well Give the three cases and, above all, the guarantee that `@AfterEach` runs even when `@BeforeEach` failed — then add the suppressed-exception aggregation and the abort-versus-fail distinction. That combination shows you have read failure reports, not just documentation.

  • A test fails an assertion and its @AfterEach then throws as well. Which exception does the report show?
    The assertion failure remains the primary reported throwable and the teardown exception is attached to it as a suppressed exception. JUnit keeps the first collected throwable as primary precisely so the original cause stays at the top of the report rather than being masked by cascading cleanup errors. Both are visible: the suppressed entries appear beneath the main stack trace.
  • How do you make a @BeforeEach method skip the test rather than fail it?
    Throw a TestAbortedException, which is what Assumptions.assumeTrue and friends do when the assumption does not hold. The engine treats an aborted test as skipped rather than failed, so a conditional such as assumeTrue(System.getProperty('os.name').startsWith('Linux')) in setup cleanly excludes tests that do not apply. A plain exception, by contrast, always reports the test as failed.

saying these in an interview costs you the question

  • Believing @AfterEach is skipped when @BeforeEach throws
  • Reporting a failed @BeforeEach as a skipped test rather than a failed one
  • Thinking an exception in @AfterEach is ignored when the test passed
  • Assuming the last exception wins rather than the first, with the rest suppressed
  • Wrapping teardown bodies in empty catch blocks to keep the suite green

context