functional-reactive
Made in the European Union

Result<T>

A Success/Failure container with map, flatMap and async combinators.

Result<T> is the central error-handling primitive of functional-reactive. Think of it as Optional<T> with a reason for the absence โ€” and a few extras Optional cannot offer because it is declared final.

Result<User> user = repository.findById(id);

user.ifPresentOrElse(
    u   -> log.info("Found {}", u),
    err -> log.warn("Lookup failed: {}", err)
);

Why a new type, when Optional exists?

Optional<T> shipped with Java 8 and is excellent at one thing: signalling absence. It has, however, a few limitations:

  1. No reason. When a value is absent, the caller has no idea why. For parsing, lookups or I/O failures the reason is exactly what you want to keep.
  2. Final. Optional is final โ€” you cannot extend or backport new methods onto it without wrapping.
  3. JDK 9 features. ifPresentOrElse, stream() and friends arrived only in Java 9. Many production projects still target older JDKs.

Result<T> is an interface, so it can be extended, backported and adapted. It carries a String error message in the Failure branch, and bridges cleanly to Optional, Stream and CompletableFuture.

Class hierarchy

Result<T>                       (interface)
   โ””โ”€โ”€ AbstractResult<T>        (abstract; holds the value, default impls)
         โ”œโ”€โ”€ Success<T>         (value present)
         โ””โ”€โ”€ Failure<T>         (value absent + error message)
  • AbstractResult<T> holds protected final T value and implements everything that does not depend on whether the result represents success or failure โ€” ifPresent, ifAbsent, isPresent, isAbsent, get, getOrElse.
  • Success<T> implements the value present branch of ifPresentOrElse, ifPresentOrElseAsync, etc. โ€” every variant simply forwards to the success action.
  • Failure<T> implements the value absent branch โ€” and additionally carries a String errorMessage describing what went wrong.

You never construct Success or Failure directly; the interface offers static factories.

Construction

MethodBehaviour
Result.success(T value)Wraps a value. The value may be null, but for safety prefer ofNullable.
Result.failure(String reason)Marks the absence of a value with a reason. Reason must be non-null.
Result.ofNullable(T value)Success if non-null, otherwise a Failure with a default message.
Result.ofNullable(T value, String reason)Same, with a custom failure reason.
Result.fromOptional(Optional<T>)Bridge from java.util.Optional.
Result<String> a = Result.success("Hello");
Result<String> b = Result.failure("no port configured");
Result<String> c = Result.ofNullable(System.getenv("PORT"));      // Success | Failure
Result<String> d = Result.fromOptional(Optional.ofNullable(env));

Transformations

Result<Integer> len = Result.success("hello")
    .map(String::length);                       // Result<Integer>

Result<Integer> parsed = Result.ofNullable(env)
    .flatMap(this::parsePort);                  // parsePort returns Result<Integer>

Result<Integer> withDefault = parsed
    .or(() -> Result.success(8080));            // recover via Supplier
  • map(Function<? super T, ? extends U>) โ€” transform the value if present; failure is propagated.
  • flatMap(Function<? super T, Result<U>>) โ€” chain another Result-returning operation; failure short-circuits.
  • or(Supplier<? extends Result<? extends T>>) โ€” fall back to another Result lazily.
  • asFailure() โ€” re-type a failure as Result<U> without copying the message manually.

Combinators

When you need to merge the current value with another value the caller provides at runtime โ€” synchronously or asynchronously:

Result<String> greeting = Result.success("Sven")
    .thenCombine(LocalTime.now(),
                 (name, time) -> Result.success(name + " @ " + time));

CompletableFuture<Result<String>> async = Result.success("Sven")
    .thenCombineAsync(LocalTime.now(),
                      (name, time) -> Result.success(name + " @ " + time));
  • thenCombine(V, BiFunction<T, V, Result<R>>) โ€” synchronous combination with an external value. The BiFunction itself returns a Result.
  • thenCombineFlat(V, BiFunction<T, V, R>) โ€” same, but the BiFunction returns a plain value which is auto-wrapped via Result.ofNullable. Use it when the combine cannot fail.
  • thenCombineAsync(V, BiFunction<T, V, Result<R>>) โ€” same as thenCombine, but the combine runs on the common ForkJoinPool and the call returns a CompletableFuture<Result<R>>.

A common pattern is to pass a Supplier<V> as the value, so the second input is computed lazily at the moment of combining.

Fluent side-effects

These three return Result<T> (not void), so they can be chained without breaking the pipeline:

result
    .ifPresent(System.out::println)              // returns Result<T>
    .ifAbsent(()   -> log.warn("nothing here")) // returns Result<T>
    .ifFailed(err -> log.warn("failed: {}", err));

Use them when you want to observe a value mid-pipeline without consuming it.

Terminal operations

result.ifPresentOrElse(
    value -> render(value),
    error -> show("error: " + error)
);

result.ifPresentOrElseAsync(
    value -> render(value),
    error -> show("error: " + error)
);

ifPresentOrElse and ifPresentOrElseAsync both return void โ€” they terminate the pipeline. The *Async variants dispatch onto the common ForkJoinPool, returning immediately. Use them when the action is independent of the calling thread (logging, fire-and-forget side effects). The synchronous variants block until the consumer returns.

Bridges

Optional<T>           toOptional();
static <T> Result<T>  fromOptional(Optional<T> opt);

Stream<T>             stream();   // zero or one element

stream() makes Result slot directly into Stream pipelines:

Stream.of("1", "2", "Hi", "3")
      .map((CheckedFunction<String, Integer>) Integer::valueOf)  // Stream<Result<Integer>>
      .flatMap(Result::stream)                                   // Stream<Integer>
      .reduce(Integer::sum)
      .ifPresent(System.out::println);                           // 6

The failure branch silently produces an empty stream โ€” which is exactly what you want when filtering out unparseable input.

When to reach for Result

Result vs. Optional
Reach for Result when absence has a reason the caller cares about โ€” failed parsing, missing config, lookups that may fail, I/O. Reach for Optional when absence itself is the only information you need.
Don't smuggle exceptions through Result
Result.failure(String) carries a message, not a Throwable. If you need the full stack trace, log it at the point of conversion (typically in a CheckedFunction) and let Result carry the human-readable reason forward.