Preconditions and Input Validation
Documenting what a method needs, and enforcing it with IllegalArgumentException
Every method you write makes assumptions about the values coming in. square(n) works for any int. sqrt(x) only makes sense when x >= 0. parseInt("abc") can’t do its job because "abc" is not a number. These assumptions — things that must be true before a method is called for the method to behave correctly — are called preconditions.
A good method does two things with its preconditions: it documents them so callers know the contract, and it enforces them so that violations produce loud, helpful errors instead of silent nonsense. The tool for both is the same tiny conditional at the top of the method.
In a nutshell
A precondition is what must be true before the method is called. Document it with a comment. Enforce it with an if + throw new IllegalArgumentException(...) at the very top of the method. Callers who obey the precondition get correct answers; callers who violate it get a runtime error that names the bad value.
A postcondition is what the method guarantees to be true after it returns (given the preconditions held). We will mention them briefly; deeper treatment is later.
By the end of this lesson you will be able to identify a method’s preconditions, write a guard clause that enforces one, and explain why throwing an exception is better than quietly returning a flag when inputs are bad.
Quick reference
The validation idiom
public static int mustBePositive(int n) {
// Precondition: n > 0
if (n <= 0) {
throw new IllegalArgumentException("n must be positive, got " + n);
}
// ... rest of the method assumes n is positive ...
}
Three rules to make this pattern right:
- The guard goes at the very top. Reject bad inputs before doing any real work.
- Comment the precondition in English. Callers should not have to read the body to know the rule.
- Include the offending value in the exception message. Future you, stepping through a stack trace at 2 a.m., will be grateful.
Common precondition patterns
Non-negative — n >= 0:
if (n < 0) {
throw new IllegalArgumentException("n must be non-negative: " + n);
}
Strictly positive — n > 0:
if (n <= 0) {
throw new IllegalArgumentException("n must be positive: " + n);
}
Within an inclusive range — 0 <= score <= 100:
if (score < 0 || score > 100) {
throw new IllegalArgumentException("score out of range 0-100: " + score);
}
Not zero — denominator != 0:
if (denominator == 0) {
throw new IllegalArgumentException("denominator must not be zero");
}
Not null — s != null:
if (s == null) {
throw new IllegalArgumentException("s must not be null");
}
Pattern rule: the guard condition is the negation of the precondition. Write the precondition in English first, then flip each comparison.
Terminology
| Term | Meaning |
|---|---|
| Precondition | What must be true before the method is called |
| Postcondition | What the method guarantees is true after it returns |
IllegalArgumentException |
Java’s built-in exception for “your argument is wrong” |
| Guard clause | A validation if at the top of a method |
| Throw | Raise an exception; if uncaught, the program stops with a stack trace |
Deep dive
1. What’s a precondition, really?
Consider a method that computes a student’s age:
public static int ageInYears(int birthYear, int currentYear) {
return currentYear - birthYear;
}
This method works only if birthYear <= currentYear. Call ageInYears(2050, 2026) and you get -24 — not a sensible age. The method ran without error. It even returned an int. But the answer is nonsense, and if your program uses it to decide whether a user is old enough to vote, the nonsense spreads.
The assumption birthYear <= currentYear is a precondition. It should be part of the method’s contract with its callers, not left for callers to guess.
There are two things a good method does with a precondition.
Document it. Write the assumption in a comment so anyone reading the method knows the rule:
// Precondition: birthYear <= currentYear
public static int ageInYears(int birthYear, int currentYear) {
return currentYear - birthYear;
}
Enforce it. Add a guard that rejects bad input at the door:
public static int ageInYears(int birthYear, int currentYear) {
// Precondition: birthYear <= currentYear
if (birthYear > currentYear) {
throw new IllegalArgumentException(
"birthYear (" + birthYear + ") must be <= currentYear (" + currentYear + ")");
}
return currentYear - birthYear;
}
Now ageInYears(2050, 2026) does not quietly return -24. It throws an IllegalArgumentException with a message naming exactly what was wrong. The program stops. A developer sees the stack trace and fixes the real bug — the code that passed the wrong year.
Self-check. A method
reverseString(String s)returns the characters ofsin reverse order. What is its precondition?Answer
s != null. The method will need to call.length()or.charAt(...)ons, and any method call on a null reference throwsNullPointerException. Notice thats == ""(the empty string) is fine — reversing an empty string is an empty string, a perfectly sensible answer. Deciding which edge cases count as “valid input” is part of writing the method’s contract.
sqrtInt(int n), a method that returns the integer square root of n?0 is fine — sqrt(0) = 0, a sensible answer — so the precondition is n >= 0, not n > 0. Option D doesn't apply: int is a primitive and can't be null.
2. The validation idiom, mechanically
Every validation guard has the same shape:
if (condition-that-says-the-input-is-BAD) {
throw new IllegalArgumentException("message naming the bad value");
}
Three mechanics that catch people.
Top of the method. The guard goes before any real work happens. Why? If something is about to blow up, blow it up immediately — before you’ve wasted computation, opened a file, or produced half a result. The rest of the method then gets to assume its inputs are clean.
The condition says “bad input,” not “good input.” Students often write this backward. Say the precondition is n > 0. The guard fires when the input is invalid, so the condition is n <= 0 (the negation of n > 0). A useful mental recipe: write the precondition first, then stick !(...) around it, then simplify. !(n > 0) simplifies to n <= 0.
Message identifies the culprit. Always include the bad value. When you hit this exception six months from now in a message from a teammate, you want to know which argument was wrong, not just that something was wrong.
Self-check. Write a guard clause for the precondition
x != 0in a method that divides byx.Answer
if (x == 0) { throw new IllegalArgumentException("x must not be zero"); }The precondition is
x != 0. Negate:x == 0. That’s what the guard checks to reject.
Self-check. You have a method:
public static int daysInMonth(int month, int year) { // Precondition: 1 <= month <= 12 // ... }Someone calls
daysInMonth(13, 2026). What should happen?Answer
It should throw
IllegalArgumentException("month out of range 1-12: 13")or similar. The method should not guess, return0, or return31. The precondition was violated, and the right response is to tell the caller loudly.
3. Why throw instead of print-and-return-a-flag?
A natural-looking alternative to throwing is to print an error and return a “sentinel” value — say -1 for an invalid age. Why is that worse?
The caller might not notice. They asked for an age; they got -1. They stored it in a variable and used it in if (age > 65) { ... }. The condition happens to be false, and the bug gets silently swallowed. No error. No trace. Nothing.
There’s no universal sentinel. What should daysInMonth(13, 2026) return to mean “invalid”? -1? 0? Both look like plausible-but-wrong answers to careless code. The function signature forces you to return some int. There’s no way to say “not a real answer, please don’t use this.”
Throwing solves both problems. The caller cannot ignore it — if they do nothing, the program stops with a stack trace pointing at the caller. They can handle the exception if they have a plan (later). But silently producing wrong answers is off the table. That difference — between a program that fails loudly and one that corrupts silently — is one of the most important reliability tools in your career.
Self-check. You’re reviewing a classmate’s code. They wrote:
public static double sqrt(double x) { if (x < 0) { System.out.println("error: negative"); return 0.0; } // ... real work ... }Name two problems.
Answer
(1) The caller might not see the printed error (it could be buried in output, or the program might be redirected to a file), and will use the returned
0.0as if it were a real square root — leading to silent bugs elsewhere. (2)0.0is a valid square root (of0), so there’s no way for careful callers to distinguish “the real answer is 0.0” from “something went wrong.” Fix: replace both lines withthrow new IllegalArgumentException("x must be non-negative: " + x);.
Reflection. Why throw an exception instead of returning
-1ornullfor invalid input? Work out a concrete story: a functiondaysUntilBirthday(int month, int day)gets called withmonth = 13, returns-1as a sentinel, and that-1gets used downstream — trace what happens three or four calls later. What does the bug look like by the time a developer has to debug it, and how does throwing an exception at the original call site change that story?
4. Postconditions — the other side of the contract
If a precondition says “what the caller promises to us,” a postcondition says “what we promise to the caller.” It usually takes the form “if the preconditions were met, the return value will have property X (and the program state will be Y).”
For sqrt(x) with precondition x >= 0, the postcondition is “returns a non-negative double whose square is approximately x.” For ageInYears(birthYear, currentYear) the postcondition is “returns a non-negative int.”
In CS1 you rarely enforce postconditions explicitly — they are usually implicit in the math. But writing them down alongside preconditions is good practice: it forces you to state what your method actually guarantees, which is the same as stating what it is for.
Before you leave
A method declares a precondition, enforces it with a tiny if + throw at the top, and from that point on assumes its inputs are clean. That small guard is the difference between a program that corrupts silently and a program that fails loudly — and loud failures are easier to fix by an order of magnitude.
Want to practice? The Week 3 quiz-prep set on the practice platform mixes if/else tracing with precondition-style return questions from past Reges midterms.