Fail Fast and Defensive Programming in Java
Introduction
This tutorial explains two related principles for handling bad data and bugs - fail fast and defensive programming - with Java examples. We will start with what failing fast means and why an error that is noticed late costs so much more. We will then run an example in which bad data is quietly accepted and crashes a report later, and compare it with a class that rejects the same data at once with a clear message. We will also see Java's own fail-fast iterators. Next, we will look at defensive programming, where it helps and where it hides bugs, and how to combine the two. Lastly, we will list the Java tools for failing fast.What does fail fast mean
A fail-fast system reports a problem the moment it detects one, and stops the operation, instead of carrying on with data it knows, or could know, is wrong. Jim Shore described the idea in his 2004 IEEE Software column Fail Fast: the most annoying bugs are the ones that show up long after their cause, so code should check its assumptions early and complain loudly.The cost of an error grows with the distance it travels. A missing department noticed while importing a file costs one log message. The same missing department noticed weeks later, when a report crashes, costs a debugging session, and a clean-up of everything that was built on the bad record in the meantime.
Example - failing late and failing fast
The program below imports two employees, one of whom has no department, in two ways.LenientEmployee accepts whatever it is given, and the problem only appears when a report later uses the department. The Employee record checks its data in its constructor. The last part shows a fail-fast feature built into the JDK's collections.
Java code comparing a lenient class that fails late with a class that fails fast
//FailFastExample.java
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.List;
import java.util.Objects;
public class FailFastExample {
// ---------- Fails late: accepts anything ----------
static class LenientEmployee {
final String name;
final String department;
final double salary;
LenientEmployee(String name, String department, double salary) {
this.name = name;
this.department = department;
this.salary = salary;
}
}
static String departmentReport(List<LenientEmployee> staff) {
StringBuilder report = new StringBuilder();
for (LenientEmployee e : staff) {
report.append(e.department.toUpperCase()).append(": ").append(e.name).append('\n');
}
return report.toString();
}
// ---------- Fails fast: rejects bad data at construction ----------
record Employee(String name, String department, double salary) {
Employee {
Objects.requireNonNull(name, "name is required");
Objects.requireNonNull(department, "department is required for " + name);
if (salary <= 0) {
throw new IllegalArgumentException(
"salary must be positive for " + name + ": " + salary);
}
}
}
public static void main(String[] args) {
List<LenientEmployee> imported = new ArrayList<>();
imported.add(new LenientEmployee("Tom Jones", "Finance", 5200));
imported.add(new LenientEmployee("Harry Major", null, 4100));
System.out.println("Lenient import: " + imported.size() + " employees accepted");
try {
System.out.print(departmentReport(imported));
} catch (NullPointerException e) {
System.out.println("Report crashed: " + e.getMessage());
}
try {
new Employee("Harry Major", null, 4100);
} catch (NullPointerException e) {
System.out.println("Fail-fast import rejected: " + e.getMessage());
}
try {
new Employee("Ethan Hardy", "Sales", -50);
} catch (IllegalArgumentException e) {
System.out.println("Fail-fast import rejected: " + e.getMessage());
}
List<String> names = new ArrayList<>(List.of("Tom Jones", "Harry Major", "Ethan Hardy"));
try {
for (String n : names) {
if (n.startsWith("Tom")) names.remove(n); // modifies the list while iterating
}
} catch (ConcurrentModificationException e) {
System.out.println("Iterator failed fast: " + e.getClass().getSimpleName());
}
names.removeIf(n -> n.startsWith("Harry")); // the safe way to remove while scanning
System.out.println("After removeIf: " + names);
}
}
Lenient import: 2 employees accepted Report crashed: Cannot invoke "String.toUpperCase()" because "e.department" is null Fail-fast import rejected: department is required for Harry Major Fail-fast import rejected: salary must be positive for Ethan Hardy: -50.0 Iterator failed fast: ConcurrentModificationException After removeIf: [Ethan Hardy]
Explanation of the code
- The lenient import accepted both employees. The problem surfaced only inside
departmentReport(), and the exception message describes the report's code, not the import that let a missing department in. In a real system the report would run days later, and the bad record would already be in the database. - The fail-fast
Employeerefused the same data in its compact constructor. The messages name the field, the employee and the bad value, at the moment the data arrives - while the file and line that caused it are still known. - Because an
Employeecan only be created with valid data, every method that receives one can rely on it. The report does not need its own null checks. - The last part shows Java's fail-fast iterators. Removing an element from an
ArrayListwhile a for-each loop is iterating over it makes the iterator throwConcurrentModificationExceptionon its next step, rather than skipping or repeating elements silently.removeIf()is the safe way to remove while scanning. - Note that the fail-fast iterator is a best-effort check. Removing the second-to-last element, for example, can end the loop before the iterator notices. It is a safety net for bugs, not a guarantee to rely on.
What is defensive programming
Defensive programming means writing code that keeps working correctly even when it is used in ways its author did not intend: it checks its inputs, protects its internal state, and copes with failures in the things it depends on. Typical defensive techniques are -- Validating inputs to public methods and constructors.
- Defensive copies of mutable arguments and return values, so callers cannot change an object's state from outside - see Immutability in JavaClick to read tutorial: Immutability in Java: Principles and Practice.
- Handling failures of external systems: timeouts on network calls, retries with limits, and clear errors when a file is missing.
- Not trusting data from outside: user input, HTTP requests, files and other services are checked before use.
When defensive programming goes wrong
Defensive programming turns harmful when "defend" comes to mean "hide the problem and carry on" -Defensive code that hides bugs
// Swallowing the exception: the payroll run "succeeds" with a missing payslip
try {
sendPayslip(employee);
} catch (Exception e) {
// ignore
}
// Inventing a default: a missing salary silently becomes 0
double salary = (record.salary() == null) ? 0 : record.salary();
// Checking the same thing everywhere: noise that hides the real logic
if (employee != null && employee.department() != null && !employee.name().isEmpty()) { ... }
Combining the two - defend the boundary, fail fast inside
The two principles fit together when you draw a trust boundary around your core code. Everything outside it - users, files, HTTP requests, other services - is checked defensively, and turned into valid domain objects or rejected with a clear error. Inside, code trusts the types it receives and fails fast, with an exception or an assertion, if an assumption is broken - because inside the boundary, a broken assumption means a bug.
EmployeeId record in DRY, KISS and YAGNIClick to read tutorial: DRY, KISS and YAGNI Principles with Java Examples carry the proof of validity with them.
Java tools for failing fast
Objects.requireNonNull(value, message): rejectsnullwith your message. Use it on constructor and method parameters.Objects.checkIndex(index, length): rejects an out-of-range index (Java 9 and later).IllegalArgumentExceptionandIllegalStateException: the standard exceptions for a bad argument and for a call made at the wrong time.- Compact constructors in records: one place to validate every way an object can be created.
List.copyOf(),List.of()and friends: rejectnullelements, so a bad element is caught when the list is built, not when it is read.assert: checks internal assumptions during development and testing. Assertions are disabled by default and switched on withjava -ea, so never use them to validate input from outside.- Helpful NullPointerException messages: since Java 14, an NPE names the expression that was null - see Java 14 Helpful NullPointerExceptionsClick to read: Java 14 Helpful NullPointerExceptions. They help when something does slip through, but a check at the boundary gives a better message, earlier.
Summary
In this tutorial we first saw what failing fast means and why the cost of an error grows with the distance it travels. Next we ran an example in which a lenient class accepted a missing department and a report crashed later, while a record with a validating constructor rejected the same data at once with a clear message - and saw Java's fail-fast iterators. We then looked at defensive programming and its useful techniques, and at the ways it goes wrong by swallowing exceptions, inventing defaults and repeating checks. We then combined the two: defend the trust boundary, and fail fast inside it. Lastly, we listed the Java tools for failing fast.Programming Principles Tutorials
Pure Functions and Side Effects with Java ExamplesClick to read tutorial: Pure Functions and Side Effects with Java Examples Design by Contract in Java with ExamplesClick to read tutorial: Design by Contract in Java with Examples SOLID Principles in Java: Overview with ExamplesClick to read tutorial: SOLID Principles in Java: Overview with Examples Composition over Inheritance in Java with ExamplesClick to read tutorial: Composition over Inheritance in Java with Examples Coupling and Cohesion Explained with Java ExamplesClick to read tutorial: Coupling and Cohesion Explained with Java Examples DRY, KISS and YAGNI Principles with Java ExamplesClick to read tutorial: DRY, KISS and YAGNI Principles with Java Examples Dependency Injection vs Dependency Inversion vs IoCClick to read tutorial: Dependency Injection vs Dependency Inversion vs IoC
Pure Functions and Side Effects with Java ExamplesClick to read tutorial: Pure Functions and Side Effects with Java Examples Design by Contract in Java with ExamplesClick to read tutorial: Design by Contract in Java with Examples SOLID Principles in Java: Overview with ExamplesClick to read tutorial: SOLID Principles in Java: Overview with Examples Composition over Inheritance in Java with ExamplesClick to read tutorial: Composition over Inheritance in Java with Examples Coupling and Cohesion Explained with Java ExamplesClick to read tutorial: Coupling and Cohesion Explained with Java Examples DRY, KISS and YAGNI Principles with Java ExamplesClick to read tutorial: DRY, KISS and YAGNI Principles with Java Examples Dependency Injection vs Dependency Inversion vs IoCClick to read tutorial: Dependency Injection vs Dependency Inversion vs IoC