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.
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:
-
JUnit starts the test method
substringWithInvalidIndexThrowsException(). -
It evaluates
assertThrows(...). -
The expected exception type is
StringIndexOutOfBoundsException.class. -
JUnit runs the lambda body:
"hello".substring(10); -
Java tries to get a substring starting at index
10. -
The string only has length
5, so Java throwsStringIndexOutOfBoundsException. -
JUnit catches that exception internally.
-
JUnit compares the actual exception type with the expected type.
-
Because they match, the assertion passes.
-
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.
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
| Approach | JUnit Version | Good For | Limitations |
|---|---|---|---|
Manual try/catch | Any | Understanding the raw mechanics | Verbose, easier to get wrong |
@Test(expected = ...) | JUnit 4 | Simple exception type checks | Cannot easily inspect message or exact throw location |
ExpectedException rule | JUnit 4 | Type and message checks | More boilerplate, older style |
assertThrows(...) | JUnit 5 and later style | Clear, flexible, modern testing | Requires lambda syntax |
assertThrows vs manual try/catch
| Feature | assertThrows | Manual try/catch |
|---|---|---|
| Readability | High | Lower |
| Boilerplate | Low | High |
| Can inspect exception | Yes | Yes |
| Easy to misuse | Less | More |
| Preferred in modern JUnit | Yes | No |
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.
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
assertThrowsin 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.
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
- Create a method that rejects blank usernames.
- Create a method that rejects negative ages.
- Write tests that assert the correct exception type is thrown.
- Verify at least one exception message.
- Use
assertThrowsinstead of manualtry/catch.
Keep learning
Related questions
Add External JAR Files to an IntelliJ IDEA Java Project
Learn how to add external JAR dependencies to an IntelliJ IDEA Java project using module libraries, and when to use Maven or Gradle instead.
Avoiding Java Code in JSP with JSP 2: EL and JSTL Explained
Learn how to avoid Java scriptlets in JSP 2 using Expression Language and JSTL, with examples, best practices, and common mistakes.
Call a Method After a Delay in Android Java
Learn how to run Java code after a delay in Android using Handler.postDelayed, manage the main thread, and cancel callbacks safely.