Unravel โบ Java concepts โบ Errors and logging
Defensive checks: assert and preconditions
Fail early and loudly: check arguments at the start of a method (Objects.requireNonNull, if (x < 0) throw new IllegalArgumentException(...)).
Learn these first
At a glance
A bouncer at the door of a method: bad input is stopped right there with a clear message, instead of wandering inside and causing a confusing failure ten calls later.
- 1this.name = Objects.requireNonNull(name, "name must not be null"); throws a NullPointerException with your message at the point of the mistake
- 2Preconditions.checkArgument(age >= 0, "age %s", age) (Guava), Assert.isTrue(...) (Spring) and Validate.notNull(...) (Commons Lang) are helper versions of the same idea
- 3assert total >= 0 : "negative total"; is checked only when the JVM runs with -eaIt is off in production by default, so it must never perform side effects or validate user input.
- 4Use argument errors (IllegalArgumentException) for bad input from the caller, IllegalStateException for "not allowed now", and Bean Validation annotations at the web boundary
- Validate at the boundary (controller, constructor, public method) and trust inside
- Use assert for "cannot happen" invariants in your own code while developing and in tests (run tests with -ea), not for checks that must always hold
- The compiler turns assert x : y into if (!$assertionsDisabled && !x) throw new AssertionError(y)
import java.util.Objects;public class Account { private final String owner; private long balance; public Account(String owner, long balance) { this.owner = Objects.requireNonNull(owner, "owner must not be null"); if (balance < 0) throw new IllegalArgumentException("balance must be >= 0 but was " + balance); this.balance = balance; } public void withdraw(long amount) { if (amount > balance) throw new IllegalStateException("insufficient funds"); balance -= amount; assert balance >= 0 : "negative balance"; }}- line 6 uses requireNonNull, the core of Defensive checks: assert and preconditions
Read the full explanation (about 2 min)
Fail early and loudly: check arguments at the start of a method (Objects.requireNonNull, if (x < 0) throw new IllegalArgumentException(...)). assert is a developer-only check that is switched off by default.
A mental model
A bouncer at the door of a method: bad input is stopped right there with a clear message, instead of wandering inside and causing a confusing failure ten calls later.
How it works
this.name = Objects.requireNonNull(name, "name must not be null"); throws a NullPointerException with your message at the point of the mistake.
Preconditions.checkArgument(age >= 0, "age %s", age) (Guava), Assert.isTrue(...) (Spring) and Validate.notNull(...) (Commons Lang) are helper versions of the same idea.
assert total >= 0 : "negative total"; is checked only when the JVM runs with -ea. It is off in production by default, so it must never perform side effects or validate user input.
Use argument errors (IllegalArgumentException) for bad input from the caller, IllegalStateException for "not allowed now", and Bean Validation annotations at the web boundary.
Going deeper
Validate at the boundary (controller, constructor, public method) and trust inside. Repeating null checks in every private method adds noise; annotations like @NonNull plus a static analyser add safety without runtime cost.
Use assert for "cannot happen" invariants in your own code while developing and in tests (run tests with -ea), not for checks that must always hold.
A message that contains the bad value ("age must be >= 0 but was -3") saves an hour of debugging. Do not include secrets.
Under the hood
The compiler turns assert x : y into if (!$assertionsDisabled && !x) throw new AssertionError(y). The static flag is set once when the class is initialised from the JVM option, so the check costs almost nothing when disabled.
Common mistakes
- Putting side effects in an
assert. - Using
assertto validate user input. - Skipping validation in constructors and then finding broken objects everywhere.
- Empty or missing error messages.
How Unravel recognises it
When you paste code, Unravel looks for these signs of defensive checks: assert and preconditions. Spotting them yourself is a good habit when you read unfamiliar code:
- Method calls:
Objects.requireNonNull()Objects.requireNonNullElse()Objects.checkIndex()Preconditions.checkArgument()Preconditions.checkState()Preconditions.checkNotNull()Assert.notNull()Validate.notNull()Assert.hasText()Assert.isTrue()Objects.isNull()Objects.nonNull() - Language constructs:
assert
Part of a learning path
- Errors, logging and tests in depth ยท previous: Exception causes and stack traces ยท next: Other logging APIs: JUL, Log4j, System.out
Related concepts
Frequently asked questions
What is Defensive checks: assert and preconditions?
Fail early and loudly: check arguments at the start of a method (Objects.requireNonNull, if (x < 0) throw new IllegalArgumentException(...)). assert is a developer-only check that is switched off by default.
What are the most common mistakes with Defensive checks: assert and preconditions?
Putting side effects in an assert. Using assert to validate user input. Skipping validation in constructors and then finding broken objects everywhere. Empty or missing error messages.
How can I see Defensive checks: assert and preconditions in real code?
Paste your own Java into Unravel and it will point out where defensive checks: assert and preconditions appears, explain how it works at the detail level you choose, and draw the flow of the code around it.