Question

How to Assert Exceptions in JUnit Tests

javaexceptionjunitjunit4assert

Question

How can I write a JUnit test in an idiomatic way to verify that a specific exception is thrown?

For example, I could write a test like this:

@Test
public void testFooThrowsIndexOutOfBoundsException() {
    boolean thrown = false;

    try {
        foo.doStuff();
    } catch (IndexOutOfBoundsException e) {
        thrown = true;
    }

    assertTrue(thrown);
}

However, this feels clumsy. I remember that JUnit provides a cleaner, more standard way to assert that code throws an exception, possibly through an annotation or an assertion method. What is the proper JUnit approach for this?

Short Answer

By the end of this page, you will understand how to test exceptions properly in JUnit, especially using assertThrows in modern JUnit and older approaches such as the expected attribute in JUnit 4. You will also learn why exception assertions matter, how to verify the exception message, and how to avoid common testing mistakes.

Concept

Testing exceptions is an important part of unit testing because many methods are expected to reject invalid input or fail in controlled ways.

In Java, exceptions are part of the method's behavior. If a method should throw an exception under certain conditions, that behavior should be tested just like a return value.

In JUnit, the goal is to make exception tests:

  • clear
  • concise
  • precise about which exception is expected
  • easy to maintain

The older manual try/catch approach works, but it is verbose and can hide mistakes. JUnit provides built-in ways to express this intent more clearly.

The most common modern approach is:

assertThrows(ExceptionType.class, () -> {
    // code that should throw
});

This is better because:

  • it clearly states the expected exception type
  • it focuses only on the code that should throw
  • it can return the exception object so you can inspect the message
  • it avoids extra boolean flags and manual failure handling

This matters in real programming because many APIs rely on exceptions for validation and error reporting. Good tests confirm that invalid states are rejected in predictable ways.

Mental Model

Think of an exception test like checking that a security alarm goes off when someone opens the wrong door.

  • The code under test is the door.
  • The exception is the alarm.
  • The JUnit assertion is your test that confirms the alarm really went off.

You do not just want to know that something happened. You want to know that:

  • the alarm went off
  • it was the correct alarm
  • it happened at the correct moment

assertThrows is like saying: When I open this specific door, I expect this exact alarm to trigger.

Take Quiz

Syntax and Examples

Modern JUnit: assertThrows

In JUnit 5, the standard way is assertThrows.

import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;

class FooTest {

    @Test
    void testFooThrowsIndexOutOfBoundsException() {
        assertThrows(IndexOutOfBoundsException.class, () -> {
            foo.doStuff();
        });
    }
}

You can also capture the exception and inspect it:

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;

class CalculatorTest {

    @Test
    void divideByZeroThrowsException() {
        ArithmeticException exception = assertThrows(ArithmeticException.class, () -> {
            int result = 10 / 0;
        });

        assertEquals("/ by zero", exception.getMessage());
    }
}

This is useful when both the exception type and message matter.

JUnit 4: expected in @Test

In older JUnit 4 tests, you may see this style:

import org.junit.Test;

public class FooTest {

    @Test(expected = IndexOutOfBoundsException.class)
    public void testFooThrowsIndexOutOfBoundsException() {
        foo.doStuff();
    }
}

This is shorter than try/catch, but it has a limitation: once the exception is thrown, the test ends, so checking the message or exact location is harder.

JUnit 4: ExpectedException rule

Another older JUnit 4 approach is:

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

public class FooTest {

    @Rule
    public ExpectedException thrown = ExpectedException.none();

    @Test
    public void testFooThrowsIndexOutOfBoundsException() {
        thrown.expect(IndexOutOfBoundsException.class);
        thrown.expectMessage("Invalid index");

        foo.doStuff();
    }
}

This was useful before assertThrows became the preferred style.

Why assertThrows is preferred

  • more explicit
  • easier to read
  • easier to verify messages and details
  • keeps the throwing code narrowly scoped

Step by Step Execution

Consider this example:

import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;

class StringUtilsTest {

    @Test
    void substringWithInvalidIndexThrowsException() {
        assertThrows(StringIndexOutOfBoundsException.class, () -> {
            "hello".substring(10);
        });
    }
}

Step by step:

  1. JUnit starts the test method substringWithInvalidIndexThrowsException().

  2. It evaluates assertThrows(...).

  3. The expected exception type is StringIndexOutOfBoundsException.class.

  4. JUnit runs the lambda body:

    "hello".substring(10);
    
  5. Java tries to get a substring starting at index 10.

  6. The string only has length 5, so Java throws StringIndexOutOfBoundsException.

  7. JUnit catches that exception internally.

  8. JUnit compares the actual exception type with the expected type.

  9. Because they match, the assertion passes.

  10. The test succeeds.

If no exception were thrown, the test would fail.

If a different exception were thrown, the test would also fail.

Real World Use Cases

Exception assertions are common in real applications whenever code must reject invalid input or unsafe operations.

Input validation

assertThrows(IllegalArgumentException.class, () -> {
    userService.createUser("");
});

Used when methods should reject blank names, invalid IDs, or negative values.

API request validation

A controller or service may throw an exception if required request data is missing.

assertThrows(IllegalStateException.class, () -> {
    orderService.submitOrder(null);
});

Collection bounds checks

assertThrows(IndexOutOfBoundsException.class, () -> {
    list.get(99);
});

Useful for verifying defensive code around lists, arrays, and indexing.

Parsing and conversion

assertThrows(NumberFormatException.class, () -> {
    Integer.parseInt("abc");
});

Used when parsing user input, CSV files, JSON values, or configuration.

Domain rule enforcement

A business rule might forbid invalid actions.

assertThrows(IllegalArgumentException.class, () -> {
    bankAccount.withdraw(-50);
});

These tests document the expected failure behavior of your system.

Take Quiz

Real Codebase Usage

In real projects, developers often use exception assertions as part of validation and defensive programming.

Common patterns

Guard clauses

Methods often fail early when arguments are invalid:

public void setAge(int age) {
    if (age < 0) {
        throw new IllegalArgumentException("Age cannot be negative");
    }
}

Test:

assertThrows(IllegalArgumentException.class, () -> person.setAge(-1));

Verifying exception messages

When the message helps users or developers diagnose a problem, teams often test it too:

IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> {
    validator.requirePositive(-5);
});

assertEquals("Value must be positive", ex.getMessage());

Narrow test scope

Developers usually keep only the throwing statement inside assertThrows:

assertThrows(IllegalStateException.class, () -> service.start());

This avoids accidentally passing because earlier setup code threw an exception.

Testing custom exceptions

assertThrows(InsufficientFundsException.class, () -> {
    account.withdraw(1000);
});

Error-handling contracts

In service layers, repositories, or utility classes, tests often verify that invalid states produce predictable exceptions. This makes the codebase safer and easier to debug.

Common Mistakes

1. Catching the exception manually without failing when nothing is thrown

Broken example:

@Test
void testSomething() {
    try {
        foo.doStuff();
    } catch (IndexOutOfBoundsException e) {
        // test passes silently
    }
}

Problem:

  • if no exception is thrown, the test still passes

Better:

assertThrows(IndexOutOfBoundsException.class, () -> foo.doStuff());

2. Wrapping too much code inside the assertion

Broken example:

assertThrows(IllegalArgumentException.class, () -> {
    setupDatabase();
    loadConfig();
    service.process(input);
});

Problem:

  • the exception might come from setup code instead of service.process(input)

Better:

setupDatabase();
loadConfig();
assertThrows(IllegalArgumentException.class, () -> service.process(input));

3. Asserting the wrong exception type

Broken example:

assertThrows(Exception.class, () -> foo.doStuff());

Problem:

  • this is too broad
  • many different failures would pass the test

Better:

assertThrows(IndexOutOfBoundsException.class, () -> foo.doStuff());

4. Using JUnit 4 style in a JUnit 5 project

Problem:

  • @Test(expected = ...) does not work in JUnit 5

Better:

  • use assertThrows

5. Forgetting to inspect useful details

Sometimes the exception type is not enough.

IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> {
    validator.checkEmail("");
});

assertEquals("Email is required", ex.getMessage());

6. Confusing checked and unchecked exceptions

Some methods throw checked exceptions, and your test may need to declare or handle setup logic carefully. assertThrows still works, but your lambda should contain the actual code that throws.

Comparisons

Exception assertion approaches in JUnit

ApproachJUnit VersionGood ForLimitations
Manual try/catchAnyUnderstanding the raw mechanicsVerbose, easier to get wrong
@Test(expected = ...)JUnit 4Simple exception type checksCannot easily inspect message or exact throw location
ExpectedException ruleJUnit 4Type and message checksMore boilerplate, older style
assertThrows(...)JUnit 5 and later styleClear, flexible, modern testingRequires lambda syntax

assertThrows vs manual try/catch

FeatureassertThrowsManual try/catch
ReadabilityHighLower
BoilerplateLowHigh
Can inspect exceptionYesYes
Easy to misuseLessMore
Preferred in modern JUnitYesNo

When should you still use try/catch?

Rarely in normal JUnit tests. It may be useful if you need highly custom assertions or special control flow, but even then, assertThrows is usually simpler.

Take Quiz

Cheat Sheet

Quick reference

JUnit 5

assertThrows(MyException.class, () -> {
    methodCall();
});

Capture the exception:

MyException ex = assertThrows(MyException.class, () -> methodCall());
assertEquals("message", ex.getMessage());

JUnit 4

@Test(expected = MyException.class)
public void testSomething() {
    methodCall();
}

Best practices

  • Prefer assertThrows in modern code.
  • Assert the most specific exception type possible.
  • Put only the statement that should throw inside the lambda.
  • Check the message if it matters.
  • Avoid boolean flags like thrown = true.
  • Avoid broad exception types like Exception.class.

Common failure cases

  • No exception thrown -> test fails
  • Wrong exception type thrown -> test fails
  • Exception thrown before the intended line -> test may be misleading if scope is too large

FAQ

How do I check that an exception is thrown in JUnit 5?

Use assertThrows:

assertThrows(IllegalArgumentException.class, () -> service.run());

How do I assert the exception message in JUnit?

Capture the returned exception:

IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> service.run());
assertEquals("Bad input", ex.getMessage());

What is the JUnit 4 way to expect an exception?

A common JUnit 4 approach is:

@Test(expected = IllegalArgumentException.class)
public void testRun() {
    service.run();
}

Why is assertThrows better than @Test(expected = ...)?

Because it is more precise and lets you inspect the exception object, including its message.

Should I catch the exception manually in a test?

Usually no. Manual try/catch is more verbose and easier to write incorrectly.

Can I assert a parent exception type?

Yes, but prefer the most specific type possible so your test is stricter and more meaningful.

What happens if no exception is thrown?

assertThrows fails the test.

Can I test custom exceptions with assertThrows?

Yes. It works with built-in and custom exception classes.

Take Quiz

Mini Project

Description

Build a small validation utility and write JUnit tests that prove invalid input throws the correct exceptions. This project demonstrates how exception assertions are used in everyday code when validating user input or enforcing business rules.

Goal

Create a Java validator class and test that it throws the correct exceptions for invalid values using idiomatic JUnit assertions.

Requirements

  1. Create a method that rejects blank usernames.
  2. Create a method that rejects negative ages.
  3. Write tests that assert the correct exception type is thrown.
  4. Verify at least one exception message.
  5. Use assertThrows instead of manual try/catch.
Take Quiz

Keep learning

Related questions

View more