Exceptions, Optional and Failing on Purpose
Java splits failure into three, and the split is the lesson.
- Checked exceptions extend
Exception. The compiler forces the caller to catch them or declare them. They are for conditions a caller can reasonably recover from: a missing file, a refused connection. - Unchecked exceptions extend
RuntimeException. Nothing forces anyone to handle them. They are for programming mistakes: a null reference, an index off the end, an argument that breaks a documented rule. - Errors extend
Error:OutOfMemoryError,StackOverflowError. The program is in trouble. Do not catch these.
public class Kinds {
public static void main(String[] args) {
// Unchecked - the code was wrong. Catching it hides a bug.
try {
int[] values = {1, 2, 3};
System.out.println(values[7]);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("unchecked: " + e.getMessage());
}
// Also unchecked, and the most common of all.
try {
String value = null;
System.out.println(value.length());
} catch (NullPointerException e) {
// Java 14+ says exactly which reference was null.
System.out.println("unchecked: " + e.getMessage());
}
// Checked - the input was wrong, and the caller can react.
try {
Integer.parseInt("twelve");
} catch (NumberFormatException e) {
System.out.println("(NumberFormatException is unchecked, for convenience)");
}
}
}
try, catch, finally - and the resource problem finally solves
import java.io.*;
public class Resources {
public static void main(String[] args) {
// try-with-resources closes everything it opened, in reverse order,
// even when the body throws - and it does not lose the original
// exception if closing also fails.
try (var reader = new BufferedReader(new StringReader("first\nsecond\n"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("read: " + line);
}
} catch (IOException e) {
System.out.println("io: " + e.getMessage());
}
// The same thing written by hand, which is what everyone got wrong
// before Java 7: the close has to be in a finally, the finally can
// throw, and that exception replaces the interesting one.
BufferedReader reader = null;
try {
reader = new BufferedReader(new StringReader("third\n"));
System.out.println("read: " + reader.readLine());
} catch (IOException e) {
System.out.println("io: " + e.getMessage());
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException ignored) {
// and here is where the real failure quietly disappears
}
}
}
}
}
Anything that implements AutoCloseable belongs in the parentheses. Writing the
finally by hand is a bug waiting to be reported.
Your own exceptions, and what to put in them
public class Validation {
// Unchecked, because a caller building a Registration has made a mistake -
// and carrying the field means the caller can report it usefully.
static final class ValidationException extends RuntimeException {
// Every Throwable is Serializable, so javac -Xlint:serial asks for this.
// It costs a line and stops the warning being one more thing to ignore.
private static final long serialVersionUID = 1L;
private final String field;
ValidationException(String field, String reason) {
super(field + " " + reason);
this.field = field;
}
String field() { return field; }
}
record Registration(String email, int age) {
Registration {
if (!email.contains("@")) throw new ValidationException("email", "must contain @");
if (age < 13) throw new ValidationException("age", "must be at least 13");
}
}
public static void main(String[] args) {
for (var input : new String[][]{{"ada@example.com", "36"}, {"nope", "30"}, {"a@b.com", "9"}}) {
try {
System.out.println(new Registration(input[0], Integer.parseInt(input[1])) + " accepted");
} catch (ValidationException e) {
System.out.println(input[0] + " rejected on " + e.field() + ": " + e.getMessage());
}
}
}
}
Never swallow, and never lose the cause
public class Causes {
static void load(String key) throws Exception {
try {
Integer.parseInt(key);
} catch (NumberFormatException e) {
// Wrap, and pass the original as the cause: the stack trace then
// shows both where it failed and what you were trying to do.
throw new Exception("cannot load key " + key, e);
}
}
public static void main(String[] args) {
try {
load("abc");
} catch (Exception e) {
System.out.println(e.getMessage());
System.out.println(" caused by: " + e.getCause());
}
// The one thing never to write:
try {
Integer.parseInt("abc");
} catch (NumberFormatException ignored) {
// silence. The program continues in a state nobody reasoned about.
}
System.out.println("(an empty catch block is how a bug becomes unfindable)");
}
}
Optional, for a return value that may not exist
Optional is a return type. It says "there may be nothing here" in the
signature, where a caller cannot miss it. It is not for fields and not for
parameters.
import java.util.*;
public class Optionals {
private static final Map<String, String> NAMES = Map.of("1", "Ada");
static Optional<String> findName(String id) {
return Optional.ofNullable(NAMES.get(id));
}
public static void main(String[] args) {
System.out.println(findName("1").orElse("unknown"));
System.out.println(findName("2").orElse("unknown"));
findName("1").ifPresentOrElse(
name -> System.out.println("found " + name),
() -> System.out.println("nothing"));
System.out.println(findName("1").map(String::toUpperCase).orElseThrow());
// orElseGet defers the work; orElse builds the fallback every time,
// even when it is not used.
System.out.println(findName("1").orElseGet(Optionals::expensiveDefault));
try {
findName("2").orElseThrow(() -> new NoSuchElementException("no name for 2"));
} catch (NoSuchElementException e) {
System.out.println(e.getMessage());
}
// What NOT to write: this is `if (x != null)` with extra steps.
var maybe = findName("1");
if (maybe.isPresent()) System.out.println(maybe.get());
}
static String expensiveDefault() {
System.out.println(" (building the default)");
return "computed";
}
}
Often the right answer is not to throw
An outcome the caller expects is a value, not an exception. Java's own library shows both spellings, and the second is usually the better neighbour.
import java.util.*;
import java.util.stream.*;
public class Values {
static OptionalInt parse(String text) {
try {
return OptionalInt.of(Integer.parseInt(text));
} catch (NumberFormatException e) {
return OptionalInt.empty();
}
}
public static void main(String[] args) {
var inputs = List.of("10", "twelve", "30");
int total = inputs.stream()
.map(Values::parse)
.filter(OptionalInt::isPresent)
.mapToInt(OptionalInt::getAsInt)
.sum();
System.out.println(total);
System.out.println(inputs.stream().map(Values::parse)
.map(o -> o.isPresent() ? String.valueOf(o.getAsInt()) : "-")
.collect(Collectors.joining(", ")));
}
}
Exceptions are also expensive: building one captures the whole stack trace. One per bad row in a million-row import is a measurable cost as well as a confusing design.
What to take away
- Checked for what a caller can recover from; unchecked for programming mistakes.
try-with-resources closes everything, in order, and keeps the real exception.- Carry the data the caller needs in your exception type, not just a message.
- Always pass the cause when wrapping; never write an empty
catch. Optionalis a return type - not a field, not a parameter.orElseGetdefers the fallback;orElsebuilds it whether or not it is needed.