Exceptions

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.

Java 8+ (Optional); Java 21 (core language)

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