Index

Functional Error Handling

Java Functional Programming

7.1 Using Either, Try Patterns (Custom Implementations)

Traditional error handling in Java primarily relies on exceptions and sometimes returning null to signal failure. While exceptions are powerful, they come with several drawbacks: they disrupt normal control flow, are often costly to create, and encourage imperative “try-catch” blocks scattered throughout the code. Returning null to indicate errors can lead to subtle bugs and NullPointerExceptions if not handled carefully.

Functional programming offers more expressive alternatives to model computations that may fail, avoiding exceptions as control flow and providing better composability. Two common patterns inspired by functional languages are Either and Try monads. They encapsulate the result of a computation that can succeed or fail, making error handling explicit and fluent.

Conceptual Model

This design explicitly separates success from failure, forcing the programmer to handle both cases without exceptions.

Simple Custom Implementation of Either

public abstract class Either<L, R> {

    public abstract boolean isRight();
    public abstract boolean isLeft();
    public abstract L getLeft();
    public abstract R getRight();

    public static <L, R> Either<L, R> right(R value) {
        return new Right<>(value);
    }

    public static <L, R> Either<L, R> left(L value) {
        return new Left<>(value);
    }

    private static final class Right<L, R> extends Either<L, R> {
        private final R value;

        Right(R value) { this.value = value; }
        public boolean isRight() { return true; }
        public boolean isLeft() { return false; }
        public L getLeft() { throw new IllegalStateException("No left value"); }
        public R getRight() { return value; }
    }

    private static final class Left<L, R> extends Either<L, R> {
        private final L value;

        Left(L value) { this.value = value; }
        public boolean isRight() { return false; }
        public boolean isLeft() { return true; }
        public L getLeft() { return value; }
        public R getRight() { throw new IllegalStateException("No right value"); }
    }
}

Usage example:

Either<String, Integer> parseInt(String s) {
    try {
        return Either.right(Integer.parseInt(s));
    } catch (NumberFormatException e) {
        return Either.left("Invalid number: " + s);
    }
}

Either<String, Integer> result = parseInt("123");
if (result.isRight()) {
    System.out.println("Parsed value: " + result.getRight());
} else {
    System.out.println("Error: " + result.getLeft());
}
Click to view full runnable Code

public abstract class Either<L, R> {

    public abstract boolean isRight();
    public abstract boolean isLeft();
    public abstract L getLeft();
    public abstract R getRight();

    public static <L, R> Either<L, R> right(R value) {
        return new Right<>(value);
    }

    public static <L, R> Either<L, R> left(L value) {
        return new Left<>(value);
    }

    private static final class Right<L, R> extends Either<L, R> {
        private final R value;

        Right(R value) { this.value = value; }

        public boolean isRight() { return true; }
        public boolean isLeft() { return false; }
        public L getLeft() { throw new IllegalStateException("No left value"); }
        public R getRight() { return value; }
    }

    private static final class Left<L, R> extends Either<L, R> {
        private final L value;

        Left(L value) { this.value = value; }

        public boolean isRight() { return false; }
        public boolean isLeft() { return true; }
        public L getLeft() { return value; }
        public R getRight() { throw new IllegalStateException("No right value"); }
    }

    // Usage example in main:
    public static void main(String[] args) {
        Either<String, Integer> result1 = parseInt("123");
        printResult(result1);

        Either<String, Integer> result2 = parseInt("abc");
        printResult(result2);
    }

    static Either<String, Integer> parseInt(String s) {
        try {
            return Either.right(Integer.parseInt(s));
        } catch (NumberFormatException e) {
            return Either.left("Invalid number: " + s);
        }
    }

    static void printResult(Either<String, Integer> result) {
        if (result.isRight()) {
            System.out.println("Parsed value: " + result.getRight());
        } else {
            System.out.println("Error: " + result.getLeft());
        }
    }
}

Simple Custom Implementation of Try

public abstract class Try<T> {

    public abstract boolean isSuccess();
    public abstract T get() throws Exception;
    public abstract Exception getException();

    public static <T> Try<T> of(CheckedSupplier<T> supplier) {
        try {
            return new Success<>(supplier.get());
        } catch (Exception e) {
            return new Failure<>(e);
        }
    }

    public interface CheckedSupplier<T> {
        T get() throws Exception;
    }

    private static final class Success<T> extends Try<T> {
        private final T value;
        Success(T value) { this.value = value; }
        public boolean isSuccess() { return true; }
        public T get() { return value; }
        public Exception getException() { return null; }
    }

    private static final class Failure<T> extends Try<T> {
        private final Exception exception;
        Failure(Exception exception) { this.exception = exception; }
        public boolean isSuccess() { return false; }
        public T get() throws Exception { throw exception; }
        public Exception getException() { return exception; }
    }
}

Usage example:

Try<Integer> result = Try.of(() -> Integer.parseInt("abc"));

if (result.isSuccess()) {
    System.out.println("Parsed: " + result.get());
} else {
    System.out.println("Failed with: " + result.getException().getMessage());
}
Click to view full runnable Code

public class TryExample {

    public static void main(String[] args) {
        Try<Integer> result = Try.of(() -> Integer.parseInt("abc"));

        if (result.isSuccess()) {
            try {
                System.out.println("Parsed: " + result.get());
            } catch (Exception e) {
                // This shouldn't happen because we already checked isSuccess()
            }
        } else {
            System.out.println("Failed with: " + result.getException().getMessage());
        }
    }

    public static abstract class Try<T> {

        public abstract boolean isSuccess();
        public abstract T get() throws Exception;
        public abstract Exception getException();

        public static <T> Try<T> of(CheckedSupplier<T> supplier) {
            try {
                return new Success<>(supplier.get());
            } catch (Exception e) {
                return new Failure<>(e);
            }
        }

        public interface CheckedSupplier<T> {
            T get() throws Exception;
        }

        private static final class Success<T> extends Try<T> {
            private final T value;

            Success(T value) {
                this.value = value;
            }

            public boolean isSuccess() {
                return true;
            }

            public T get() {
                return value;
            }

            public Exception getException() {
                return null;
            }
        }

        private static final class Failure<T> extends Try<T> {
            private final Exception exception;

            Failure(Exception exception) {
                this.exception = exception;
            }

            public boolean isSuccess() {
                return false;
            }

            public T get() throws Exception {
                throw exception;
            }

            public Exception getException() {
                return exception;
            }
        }
    }
}

Benefits of Either and Try

Summary

By adopting Either and Try patterns in Java, you gain a more functional and robust way to handle errors that avoids the pitfalls of exceptions and nulls. These patterns encourage explicit error propagation, reduce boilerplate try-catch blocks, and foster composable, maintainable code that clearly distinguishes success from failure.

Index

7.2 Function Composition with Error Handling

In functional programming, composing small, reusable functions is a key principle. However, when these functions can fail—returning errors instead of plain values—composition becomes more challenging. Traditional exception handling often breaks the flow and requires verbose try-catch blocks, making the code harder to read and maintain.

Using functional error handling constructs like Either or Try (introduced in the previous section) enables elegant composition by representing computations that may succeed or fail explicitly. This allows chaining operations safely, propagating errors automatically, and preserving error context without throwing exceptions.

The challenge: chaining computations that might fail

Suppose you have functions that return Either<L, R> or Try<T>. Composing them requires passing the successful result from one function to the next while short-circuiting the pipeline if an error occurs.

Using Either for composition with flatMap

Recall our custom Either<L, R> implementation from the previous section. To compose functions, we add a flatMap method:

public abstract class Either<L, R> {
    // ... existing methods ...

    public abstract <U> Either<L, U> flatMap(Function<? super R, Either<L, U>> mapper);

    // Inside Right:
    private static final class Right<L, R> extends Either<L, R> {
        // ...
        public <U> Either<L, U> flatMap(Function<? super R, Either<L, U>> mapper) {
            return mapper.apply(value);
        }
    }

    // Inside Left:
    private static final class Left<L, R> extends Either<L, R> {
        // ...
        public <U> Either<L, U> flatMap(Function<? super R, Either<L, U>> mapper) {
            return (Either<L, U>) this; // propagate the error without calling mapper
        }
    }
}

Example: Composing functions with Either

Either<String, Integer> parse(String s) {
    try {
        return Either.right(Integer.parseInt(s));
    } catch (NumberFormatException e) {
        return Either.left("Invalid number: " + s);
    }
}

Either<String, Integer> reciprocal(int x) {
    if (x == 0) {
        return Either.left("Division by zero");
    } else {
        return Either.right(1 / x);
    }
}

public void composeExample() {
    Either<String, Integer> result = parse("10")
        .flatMap(this::reciprocal);

    result.isRight()
        ? System.out.println("Result: " + result.getRight())
        : System.out.println("Error: " + result.getLeft());
}

Here, if parse fails, the error propagates, and reciprocal is never called, short-circuiting the chain. If both succeed, the final result is printed.

Using Try for safe composition

Similarly, a Try type can have a flatMap method to chain computations:

public abstract class Try<T> {
    // ... existing methods ...

    public abstract <U> Try<U> flatMap(Function<? super T, Try<U>> mapper);

    // Success implementation:
    private static final class Success<T> extends Try<T> {
        // ...
        public <U> Try<U> flatMap(Function<? super T, Try<U>> mapper) {
            try {
                return mapper.apply(value);
            } catch (Exception e) {
                return new Failure<>(e);
            }
        }
    }

    // Failure implementation:
    private static final class Failure<T> extends Try<T> {
        // ...
        public <U> Try<U> flatMap(Function<? super T, Try<U>> mapper) {
            return (Try<U>) this;
        }
    }
}

Example usage:

Try<Integer> parseTry(String s) {
    return Try.of(() -> Integer.parseInt(s));
}

Try<Integer> reciprocalTry(int x) {
    return x == 0
        ? new Try.Failure<>(new ArithmeticException("Division by zero"))
        : new Try.Success<>(1 / x);
}

public void tryComposeExample() {
    Try<Integer> result = parseTry("0")
        .flatMap(this::reciprocalTry);

    if (result.isSuccess()) {
        System.out.println("Result: " + result.get());
    } else {
        System.out.println("Failed: " + result.getException().getMessage());
    }
}

Recovering from errors

Both Either and Try support recovery methods:

Example recovery with Try:

Try<Integer> safeResult = parseTry("abc")
    .flatMap(this::reciprocalTry)
    .recover(ex -> 0); // fallback to 0 on failure

Summary

By adopting these patterns, Java developers gain powerful tools for robust error propagation and composition, moving beyond the pitfalls of exceptions and nulls.

Index

7.3 Example: Validating User Input Functionally

Validating user input is a common task that often involves multiple checks on different fields. Traditional validation approaches either throw exceptions on the first failure or accumulate errors in an ad hoc manner. Functional error handling patterns like Either allow us to build composable validation pipelines that clearly separate success and failure cases, enabling us to collect all errors without exceptions.

In this example, we'll create a simple user input validator that checks a username and age, accumulating errors if either validation fails. We’ll use a custom Either type that supports error accumulation in a List<String> on the left side.

Custom Either supporting error accumulation

import java.util.*;
import java.util.function.Function;

abstract class Either<L, R> {
    public abstract boolean isRight();
    public abstract R getRight();
    public abstract L getLeft();

    // Factory methods
    public static <L, R> Either<L, R> right(R value) {
        return new Right<>(value);
    }

    public static <L, R> Either<L, R> left(L value) {
        return new Left<>(value);
    }

    // Combine two Eithers accumulating errors (assuming L is List<String>)
    public Either<List<String>, R> combine(Either<List<String>, R> other, 
                                          Function<R, R> combineFunc) {
        if (this.isRight() && other.isRight()) {
            return right(combineFunc.apply(other.getRight()));
        } else {
            List<String> errors = new ArrayList<>();
            if (this.isLeft()) errors.addAll((List<String>)this.getLeft());
            if (other.isLeft()) errors.addAll((List<String>)other.getLeft());
            return left(errors);
        }
    }

    // Inner classes
    private static final class Right<L, R> extends Either<L, R> {
        private final R value;
        Right(R value) { this.value = value; }
        public boolean isRight() { return true; }
        public R getRight() { return value; }
        public L getLeft() { throw new NoSuchElementException("No Left value"); }
    }

    private static final class Left<L, R> extends Either<L, R> {
        private final L value;
        Left(L value) { this.value = value; }
        public boolean isRight() { return false; }
        public R getRight() { throw new NoSuchElementException("No Right value"); }
        public L getLeft() { return value; }
    }
}

User and validation functions

class User {
    final String username;
    final int age;

    User(String username, int age) {
        this.username = username;
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{username='" + username + "', age=" + age + "}";
    }
}
public class UserValidationExample {

    static Either<List<String>, String> validateUsername(String username) {
        List<String> errors = new ArrayList<>();
        if (username == null || username.isEmpty()) {
            errors.add("Username cannot be empty");
        }
        if (username != null && username.length() < 3) {
            errors.add("Username must be at least 3 characters");
        }
        return errors.isEmpty() ? Either.right(username) : Either.left(errors);
    }

    static Either<List<String>, Integer> validateAge(int age) {
        if (age < 18) {
            return Either.left(Collections.singletonList("Age must be at least 18"));
        }
        return Either.right(age);
    }

    static Either<List<String>, User> validateUser(String username, int age) {
        Either<List<String>, String> validUsername = validateUsername(username);
        Either<List<String>, Integer> validAge = validateAge(age);

        if (validUsername.isRight() && validAge.isRight()) {
            return Either.right(new User(validUsername.getRight(), validAge.getRight()));
        }

        // Accumulate errors from both validations
        List<String> allErrors = new ArrayList<>();
        if (validUsername.isLeft()) allErrors.addAll(validUsername.getLeft());
        if (validAge.isLeft()) allErrors.addAll(validAge.getLeft());

        return Either.left(allErrors);
    }

    public static void main(String[] args) {
        // Test cases
        Either<List<String>, User> user1 = validateUser("Al", 20);
        Either<List<String>, User> user2 = validateUser("Alice", 17);
        Either<List<String>, User> user3 = validateUser("", 15);
        Either<List<String>, User> user4 = validateUser("Bob", 25);

        printResult(user1);
        printResult(user2);
        printResult(user3);
        printResult(user4);
    }

    static void printResult(Either<List<String>, User> result) {
        if (result.isRight()) {
            System.out.println("Validation succeeded: " + result.getRight());
        } else {
            System.out.println("Validation failed with errors:");
            result.getLeft().forEach(err -> System.out.println(" - " + err));
        }
    }
}
Click to view full runnable Code

import java.util.*;
import java.util.function.Function;

// Either abstraction
abstract class Either<L, R> {
    public abstract boolean isRight();
    public abstract R getRight();
    public abstract L getLeft();

    public static <L, R> Either<L, R> right(R value) {
        return new Right<>(value);
    }

    public static <L, R> Either<L, R> left(L value) {
        return new Left<>(value);
    }

    private static final class Right<L, R> extends Either<L, R> {
        private final R value;
        Right(R value) { this.value = value; }
        public boolean isRight() { return true; }
        public R getRight() { return value; }
        public L getLeft() { throw new NoSuchElementException("No Left value"); }
    }

    private static final class Left<L, R> extends Either<L, R> {
        private final L value;
        Left(L value) { this.value = value; }
        public boolean isRight() { return false; }
        public R getRight() { throw new NoSuchElementException("No Right value"); }
        public L getLeft() { return value; }
    }
}

// User class
class User {
    final String username;
    final int age;

    User(String username, int age) {
        this.username = username;
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{username='" + username + "', age=" + age + "}";
    }
}

// Validation and demo logic
public class UserValidationExample {

    static Either<List<String>, String> validateUsername(String username) {
        List<String> errors = new ArrayList<>();
        if (username == null || username.isEmpty()) {
            errors.add("Username cannot be empty");
        }
        if (username != null && username.length() < 3) {
            errors.add("Username must be at least 3 characters");
        }
        return errors.isEmpty() ? Either.right(username) : Either.left(errors);
    }

    static Either<List<String>, Integer> validateAge(int age) {
        if (age < 18) {
            return Either.left(Collections.singletonList("Age must be at least 18"));
        }
        return Either.right(age);
    }

    static Either<List<String>, User> validateUser(String username, int age) {
        Either<List<String>, String> validUsername = validateUsername(username);
        Either<List<String>, Integer> validAge = validateAge(age);

        if (validUsername.isRight() && validAge.isRight()) {
            return Either.right(new User(validUsername.getRight(), validAge.getRight()));
        }

        List<String> allErrors = new ArrayList<>();
        if (!validUsername.isRight()) allErrors.addAll(validUsername.getLeft());
        if (!validAge.isRight()) allErrors.addAll(validAge.getLeft());

        return Either.left(allErrors);
    }

    public static void main(String[] args) {
        List<Either<List<String>, User>> testCases = Arrays.asList(
            validateUser("Al", 20),
            validateUser("Alice", 17),
            validateUser("", 15),
            validateUser("Bob", 25)
        );

        for (Either<List<String>, User> result : testCases) {
            printResult(result);
        }
    }

    static void printResult(Either<List<String>, User> result) {
        if (result.isRight()) {
            System.out.println("✅ Validation succeeded: " + result.getRight());
        } else {
            System.out.println("❌ Validation failed with errors:");
            result.getLeft().forEach(err -> System.out.println(" - " + err));
        }
    }
}

Explanation

Sample Output

Validation failed with errors:
 - Username must be at least 3 characters

Validation failed with errors:
 - Age must be at least 18

Validation failed with errors:
 - Username cannot be empty
 - Age must be at least 18

Validation succeeded: User{username='Bob', age=25}

This example demonstrates how functional error handling with Either enables expressive, composable validation pipelines that accumulate errors cleanly and improve code readability and robustness.

Index