Skip to main content
pragmatic clean code minimizing cognitive load in production java

Exceptions vs. Control Flow

3 min read Chapter 13 of 25
Summary

Exceptions handle unrecoverable system errors but are expensive...

Exceptions handle unrecoverable system errors but are expensive and obscure flow. Result types explicitly model expected failures via sealed interfaces, enabling exhaustive pattern matching. This promotes local reasoning, reduces cognitive load, and aligns with Java 21's ADTs for maintainable error handling.

Exceptions vs. Control Flow

Exceptions and result types are two distinct approaches to handling errors in software development. While exceptions are typically used for exceptional circumstances, result types are better suited for expected failures. In this section, we will explore the differences between these two approaches and provide guidance on when to use each.

Exceptions

Exceptions are a mechanism for handling exceptional circumstances, such as hardware failure or missing configuration. They are typically used when an error is unexpected and cannot be recovered from. However, using exceptions for control flow is considered an anti-pattern, as it obscures the intended logic path and can lead to significant performance overhead due to stack trace generation.

Result Types

Result types, on the other hand, are a functional programming pattern that uses a wrapper object to explicitly return either a success value or a failure object. This approach promotes ‘Local Reasoning’ by making error handling part of the function signature rather than a secondary side-channel. Result types are particularly useful for handling expected failures, such as a missing user or invalid input.

Comparison

The following table compares exceptions and result types:

FeatureExceptionsResult Types
UsageExceptional/System errorsBusiness logic failures
VisibilityHidden (Side-channel)Explicit (Method Signature)
PerformanceExpensive (Stack trace)Cheap (Object allocation)
Control FlowImplicit (Unstructured)Explicit (Structured)
Compiler HelpChecked (Partial)Exhaustive switch (Full)

Implementation

Here is an example implementation of a result type using Java 21 sealed interfaces and records:

public sealed interface Result<T> {
    record Success<T>(T value) implements Result<T> {}
    record Failure<T>(String message, ErrorCode code) implements Result<T> {}
}

public Result<User> findUser(String id) {
    if (id == null) return new Result.Failure<>("ID null", ErrorCode.INVALID);
    var user = repository.findById(id);
    return user != null 
        ? new Result.Success<>(user)
        : new Result.Failure<>("User not found", ErrorCode.NOT_FOUND);
}

// Usage with Pattern Matching (Java 21)
var output = switch(findUser("123")) {
    case Result.Success<User> s -> s.value().toString();
    case Result.Failure<User> f -> "Error: " + f.message();
};

Conclusion

In conclusion, exceptions and result types are two distinct approaches to handling errors in software development. While exceptions are suitable for exceptional circumstances, result types are better suited for expected failures. By using result types, developers can promote ‘Local Reasoning’, reduce cognitive load, and improve maintainability. It is essential to understand the differences between these two approaches and use them appropriately to ensure robust and maintainable software systems.

Sources