Magnus' blog


Result<T, E> in Java

A Result is a useful way to describe an action that can either succeed or fail without requiring the use of exceptions. In functional programming it is known as an Either pattern. This blog post presents a simple implementation of Result<T, E> in Java that has low complexity and allows for switch pattern matching.

This post contains a simplified version of the full library available on GitHub: https://github.com/HHMagnus/Java-Result

The simplest form of the implementation is:

public sealed interface Result<T, E> {
    public record Ok<T, E>(T value) implements Result<T, E> {}
    public record Err<T, E>(E error) implements Result<T, E> {}
}

Using a sealed interface, it is effectively a discriminated union with Ok and Err being the only two options. Anyone using the Result can switch pattern match it. The following example handles the errors with an exception:

switch (result) {
    case Ok(var value) -> value;
    case Err(var error) -> throw new IllegalArgumentException(error);
}

This is quite a useful pattern because it helps prevent throwing errors too early. Consider, for example, that you have a domain method that does some string-handling logic to determine if a string is valid input:

enum Error {
    IS_BLANK,
    IS_PROFANE
}

Result<String, Error> handleInput(String input) {
    final var trimmed = input.trim();
    if (trimmed.isBlank()) {
        return Result.Err(Error.IS_BLANK);
    }

    if (isProfane(trimmed)) {
        return Result.Err(Error.IS_PROFANE);
    }

    return Result.Ok(trimmed);
}

Without the Result, this would have required two throws. Because it is instead described as a Result, it can very easily be handled differently:

void ignoreCase(String input) {
    final var result = handleInput(input);

    final var value = switch (result) {
        case Ok(var val) -> val;
        case Err(var val) -> "Default value";
    };

    // Proceed with other tasks
}

void rejectionCase(String input) {
    final var result = handleInput(input);

    if (!(result instanceof Result.Ok(val))) {
        System.out.println("Did nothing because of an error: " + result);
        return;
    }

    // Proceed with other tasks
}

Fluent like Optional

Result can be seen as an extension of Optional where the empty state is instead an error. Optional provides fluent functions like map and filter that allow for handling the case where a value is present. This can be done similarly with Result:

public sealed interface Result<T, E> {
    <N> Result<N, E> map(Function<T, N> mapper);

    public record Ok<T, E>(T value) implements Result<T, E> {
        @Override
        <N> Result<N, E> map(Function<T, N> mapper) {
            final var newValue = mapper.apply(value);
            return Result.Ok(newValue);
        }
    }
    public record Err<T, E>(E error) implements Result<T, E> {
        @Override
        <N> Result<N, E> map(Function<T, N> mapper) {
            // No mapping happens as it is an error
            return Result.Err(error);
        }
    }
}

By having polymorphism support whether it is failed or ok, the implementation is quite simple:

  • In the ok case, it applies the mapper and returns the value wrapped in an Ok.
  • In the error case, it does nothing and returns the same error wrapped in a new Err to support the new type.

Beyond map, a few additional fluent methods round out the API:

  • flatMap(Function<T, Result<N, E>> mapper) — like map, but for when the mapping function itself returns a Result. This avoids ending up with a nested Result<Result<N, E>, E>. In the Ok case it applies the mapper and returns its result directly; in the Err case it passes the error through unchanged.
  • consume(Consumer<T> consumer) — runs a side-effecting action (such as logging or saving) on the value if it is Ok, and does nothing if it is Err. Returns this so chaining can continue.
  • consumeError(Consumer<E> consumer) — the mirror of consume: runs a side-effecting action on the error if it is Err, and does nothing if it is Ok. Also returns this.

Together, these allow a Result to be handled in a single fluent chain:

new Result.Ok(input)
    .map(String::trim)                  // transform the value if Ok
    .flatMap(this::handleInput)         // validate, returning Ok or Err
    .consume(System.out::println)       // print the value if still Ok
    .consumeError(System.err::println); // print the error if Err

This uses the previous handleInput function to either produce a cleaned string or an error, then routes the outcome to the appropriate output stream without a single if or try/catch.

OptionalResult and VoidResult

Two additional implementations are:

  • OptionalResult — same as Result, but with a third state for being Empty
  • VoidResult — same as Result, but always empty/void

By implementing these inside the same library as Result, one can transition between them very easily:

new Result.Ok(value)
    .toOptionalResult() // wraps in Optional
    .toVoidResult()     // removes entirely

OptionalResult provides a way to fluently transform a value if it is present without failing when something is empty. Take the following code:

new OptionalResult.Present(value)
    .mapValue(String::trim)  // removes whitespace
    .filter(String::isBlank) // defaults to Empty if blank

If the value is empty, it will default to an empty OptionalResult.

VoidResult is a way to describe an action that returns nothing but can fail:

VoidResult handleInput(String input) {
    final var trimmed = input.trim();
    if (trimmed.isBlank()) {
        return VoidResult.Err(Error.IS_BLANK);
    }

    if (isProfane(trimmed)) {
        return VoidResult.Err(Error.IS_PROFANE);
    }

    return VoidResult.Ok();
}

The library contains more examples of this.

Conclusion

The Result pattern is something I use heavily in my projects, and this new implementation is very simple while allowing complex flows to be described clearly. It is very well-suited for describing domain logic without using exceptions, allowing the consumer to specify what should happen when something goes wrong.

An implementation with many helper functions can be found on my GitHub: https://github.com/HHMagnus/Java-Result

View next or previous post: