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:
- 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.
- Final.
Optionalisfinalโ you cannot extend or backport new methods onto it without wrapping. - 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>holdsprotected final T valueand 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 ofifPresentOrElse,ifPresentOrElseAsync, etc. โ every variant simply forwards to the success action.Failure<T>implements the value absent branch โ and additionally carries aString errorMessagedescribing what went wrong.
You never construct Success or Failure directly; the interface offers static factories.
Construction
| Method | Behaviour |
|---|---|
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 anotherResult-returning operation; failure short-circuits.or(Supplier<? extends Result<? extends T>>)โ fall back to anotherResultlazily.asFailure()โ re-type a failure asResult<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. TheBiFunctionitself returns aResult.thenCombineFlat(V, BiFunction<T, V, R>)โ same, but theBiFunctionreturns a plain value which is auto-wrapped viaResult.ofNullable. Use it when the combine cannot fail.thenCombineAsync(V, BiFunction<T, V, Result<R>>)โ same asthenCombine, but the combine runs on the commonForkJoinPooland the call returns aCompletableFuture<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 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.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.Related
- Checked Functions โ lift throwing code into
Result. - CompletableFutureQueue โ chain async transformations.
- Tuples โ useful payloads for
thenCombine. - Tutorial: Error handling with Result.