78. Optional vs Exceptions
Optional models the absence of a value as a return type โ it is not a substitute for exceptions that report genuine failures; use Optional for expected absence (not-found), and exceptions for unexpected errors.
Explanation
The key semantic distinction: Optional.empty() means "successfully found nothing" โ the operation completed and the result is absent. An exception means "the operation itself failed" โ an error occurred. Use them for different things.
Use Optional as return type when: absence is a normal, expected outcome (findById, findFirst, optional config lookup). Do not use Optional when: the absence indicates a bug (throw IllegalStateException instead), when the parameter or field type has known overhead, or inside performance-sensitive collection operations (boxing overhead).
Null vs Optional vs Exception: null is the historical Java way but provides no hint to callers. Optional communicates absence explicitly in the type signature. An exception communicates failure. Return null only in performance-critical internal code and document it carefully; in public APIs, prefer Optional for absent results and exceptions for errors.
Example
import java.util.*;
public class OptionalVsExceptions {
record User(long id, String name) {}
// Return Optional: absence is normal (user may not exist)
static Optional<User> findUserById(long id, List<User> users) {
return users.stream().filter(u -> u.id() == id).findFirst();
}
// Throw exception: absence indicates a bug / contract violation
static User requireUserById(long id, List<User> users) {
return findUserById(id, users)
.orElseThrow(() -> new NoSuchElementException("User not found: " + id));
}
// Configuration: Optional for optional config, exception for required
static Optional<String> getOptionalConfig(String key) {
Map<String, String> config = Map.of("theme", "dark");
return Optional.ofNullable(config.get(key));
}
static String getRequiredConfig(String key) {
return getOptionalConfig(key)
.orElseThrow(() -> new IllegalStateException("Required config missing: " + key));
}
public static void main(String[] args) {
var users = List.of(new User(1, "Alice"), new User(2, "Bob"));
// Optional: expected absence
Optional<User> found = findUserById(1, users);
found.ifPresent(u -> System.out.println("Found: " + u.name()));
Optional<User> missing = findUserById(99, users);
System.out.println("Missing: " + missing.orElse(null)); // null โ no exception
// Exception: unexpected absence (bug in caller)
try {
requireUserById(99, users); // throws
} catch (NoSuchElementException e) {
System.out.println("Error (expected in production): " + e.getMessage());
}
// Config
System.out.println("Theme: " + getOptionalConfig("theme").orElse("light"));
System.out.println("Lang: " + getOptionalConfig("lang").orElse("en")); // default
try {
getRequiredConfig("db.url"); // required โ throws
} catch (IllegalStateException e) {
System.out.println("Required config missing: " + e.getMessage());
}
}
}Sources
- https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Optional.html โ Optional API
- https://www.pearson.com/en-us/subject-catalog/p/effective-java/P200000000138 โ Effective Java, Item 55