Understanding Assertions
Assertions test conditions that should definitely be true in a correct program and mainly help the programmer find bugs during testing and debugging
A Bug That Only Appears After Certain Actions
Imagine a bug report that says the program fails only when a user performs one particular sequence of actions. Nothing about any single action looks wrong on its own. The developer has to find the exact point where the program first goes wrong, and that is hard to do by reading the code alone. Assertions are one of the tools a programmer uses to make that search easier.
This article explains what assertions are for, how they differ from exceptions, and how to decide which one a particular check deserves. The central question is always the same: who is the check for?
Two Tools for Broken Expectations
Every non-trivial program makes promises to itself and to the people who call its code. A method may expect its input to be non-negative, or a variable may be required to stay within a certain range. When one of these expectations is broken, the program needs some way to react. Java gives programmers two different tools for this job: assertions and exceptions.
The two look similar on the surface, because both test whether a condition holds. They differ in audience, in when they are active, and in what kind of condition they usually guard. Choosing correctly between them is a design decision that shapes how a program is debugged and how it is used.
Asking Who the Check Is For
A useful way to keep the two apart is to ask who the check is for. An assertion is mostly a message to yourself. It records a belief you hold about your own code, such as a value that should never be negative at a particular point. Its job is to help you find the spot where a bug appears.
An exception is a message to whoever uses your code. When a caller hands a method an illegal argument, the exception tells that caller that they are using the method incorrectly. Thinking about the reader of the failure, rather than only the kind of failure, usually makes the right choice clear.
Read the flowchart as a sequence of questions. First ask whether the failing condition is a belief about your own code. If it is, the right tool is an assertion, because a failure there means a bug somewhere in your program. If the failing condition is about what a caller supplied, the right tool is an exception, because the fault lies with the caller's use of the method and the caller needs to be told.
Following a Check Through a Method Call
The following class is an original teaching example, not taken from the source. It contains one check that guards a public method against bad input and one assertion that records a belief about the class's own state. Read it first, then trace what happens for each kind of failure.
public class Account {
private int balance;
public Account(int initialBalance) {
if (initialBalance < 0) {
throw new IllegalArgumentException("initial balance must not be negative");
}
balance = initialBalance;
assert balance >= 0 : "balance is negative right after construction";
}
public void withdraw(int amount) {
if (amount < 0 || amount > balance) {
throw new IllegalArgumentException("withdrawal amount is not allowed");
}
balance = balance - amount;
assert balance >= 0 : "invariant broken: balance went negative";
}
}- 1Line 4 to 6: the constructor rejects a negative starting balance with an exception, because the caller supplied a value the class does not accept.
- 2Line 7 the constructor then stores the balance and asserts that it is non-negative. Given the check above, this assertion can only fail if the code itself is wrong.
- 3Line 10 to 12: withdraw rejects an amount that is negative or larger than the balance. A caller who asks for an overdraft is using the method incorrectly, so the exception is the right message.
- 4Line 13 to 14: the balance is updated and then the assertion confirms the invariant. If this assertion ever fails, the bug is inside the class, and the developer should look at this exact line and the logic leading to it.
- The if-throw checks in the constructor and withdraw guard the public contract. They run no matter how the program is launched.
- The assert statements record beliefs about the class's own state, not about what a caller is allowed to do.
- The assertion after the subtraction in withdraw is a postcondition: once the method has run its checks and updated the balance, the balance should never be negative.
Trace the two kinds of failure separately. If a caller writes account.withdraw(500) on an account holding 100, the exception fires at the first check. The message is aimed at the caller, and the caller is the one who must change what they pass in. If instead some later edit to withdraw broke the subtraction logic so that the balance became negative, the first check would not catch it, and the assertion would fire instead. That message is aimed at the developer, and it points to the exact place where the belief about the class was violated.
Postconditions and Invariants as a Natural Fit
Postconditions and invariants are a natural use for assertions. A postcondition is a condition that should hold when a method finishes. An invariant is a condition that should hold about an object's state at the points where it is meant to be stable. In a correct program, these conditions are always true, so a failure means a bug. Because a failed assertion always signals a bug, checking these conditions does not add a noticeable cost in normal execution.
This is why the withdraw example places its assertion at the end of the method. Once the checks have passed and the subtraction has happened, the balance must be non-negative. The assertion documents that expectation and gives the developer a precise location to inspect if the expectation is ever broken.
Mistakes From Mixing Up the Two Mechanisms
Most errors with these tools come from asking the wrong question about who the check serves. The following mistakes are common and each one has a specific consequence.
Common mistake Guarding a caller's illegal argument with an assertion
What it looks likeassert amount <= balance at the start of withdraw, with no if-throw check before it
Why it failsAssertions are mostly ignored after testing, so the caller's illegal request is no longer rejected. The method then carries on with bad input and the failure appears somewhere far from the cause.
Do this insteadEnforce the contract of a public method with an exception, which alerts the caller whether or not assertions are enabled.
Common mistake Guarding an internal bug with an exception
What it looks likethrow new IllegalStateException when the balance is found negative after the subtraction
Why it failsThe exception treats a bug in the class as if it were a problem with the caller's input. The message points at the wrong reader and hides the fact that the code itself is broken.
Do this insteadRecord the belief about your own code with an assertion so the failure message is aimed at the developer debugging the class.
Common mistake Assuming a public method's assertion will protect library users
Why it failsThe source notes that Oracle's documentation advises against using assertions to check the contract of public methods. Users of a library cannot rely on an assertion to reject their input.
Common mistake Believing an assertion fixes the bug it reports
Common mistake Treating every failing check as the same kind of failure without asking who reads the message
Caller Versus Programmer at a Glance
The next diagram compares the two mechanisms along the dimensions the source emphasizes: audience, when the check is active, and the kind of condition it usually guards.
Sorting Checks Into the Right Mechanism
A method named addItem in a shopping cart class receives a quantity argument. Two checks are proposed: (1) the quantity must be greater than zero, because the caller may pass zero or a negative number; (2) after adding the item, the cart's internal total must equal the sum of its item prices, which the class maintains itself. For each check, decide whether it should be an assertion or an exception, and explain your choice by naming who the failure message is for.
Hints
- Ask first whether the condition is something a caller could legitimately get wrong.
- Ask whether the condition is a belief about the class's own internal state that should hold after the method finishes.
- Recall that an assertion is mostly a message to yourself, while an exception is a message to whoever uses your code.
Key Takeaways
- Assertions test conditions that should definitely be true in a correct program and mainly help the programmer find bugs during testing and debugging.
- An assertion is mostly a message to yourself about your own code, while an exception is a message to the caller who uses a public method.
- Exceptions enforce the contract of public methods regardless of whether assertions are enabled, so caller-supplied illegal input belongs in an exception.
- Postconditions and invariants are a natural fit for assertions, because a failure means a bug without adding a noticeable cost in normal execution.
- Mixing up the two mechanisms leaves caller errors unguarded or hides internal bugs behind the wrong kind of message.