A type-safe Result library for Java, inspired by Rust's Result<T, E>. Instead of throwing exceptions, methods return explicit success or failure values — making error handling visible, composable, and impossible to accidentally ignore.
- Pattern matching — use
switchexpressions directly over result types - Sealed interfaces and records — no class hierarchies; everything is a closed, known type
- Fluent chaining — convert and transform across all three result types
- Serializable by default — as long as your generic types are serializable
- 100% test coverage — quite easy without branching
- List and combination support — stream over lists of results or combine them into a single result.
| Type | States | Use when |
|---|---|---|
Result<T, E> |
Ok(T) or Err(E) |
An operation that either succeeds with a value or fails |
OptionalResult<T, E> |
Present(T), Empty, or OptErr(E) |
An operation that may succeed with a value, succeed with nothing, or fail |
VoidResult<E> |
VoidOk or VoidErr(E) |
An operation that either succeeds (with no value) or fails |
Import into a gradle project using:
implementation 'dev.mhh:result:1.1.0'Instead of throwing an exception, return a Result:
Result<String, String> parseInput(String input) {
var trimmed = input.trim();
if (trimmed.isBlank()) return Result.err("Input is required");
return Result.ok(trimmed);
}Callers decide how to handle the result — with a switch expression, fluent chain, or any combination:
// Pattern match: handle each case explicitly
Json restEndpoint(String input) {
return switch (parseInput(input)) {
case Ok(var value) -> buildSuccess(value);
case Err(var err) -> buildError(err);
};
}
// Or throw in contexts where the error is truly unexpected
void internalMethod(String input) {
switch (parseInput(input)) {
case Ok(var value) -> process(value);
case Err(var err) -> throw new IllegalStateException("Unexpected error: " + err);
}
}Results can be transformed and converted across types in a single chain, without intermediate variables or defensive checks at each step:
VoidResult<String> result = Result.ok(rawInput)
.map(String::trim)
.mapToOptional(s -> Optional.of(s).filter(Predicate.not(String::isBlank)))
.toResult(() -> "Input must not be blank")
.verify(InputVerifier::verify)
.toVoidResult();If any step produces an error, the rest of the chain short-circuits — the error is carried through unchanged.
ResultCollector is a standard Collector that partitions a stream of Result values into either a list of all successes or a list of all errors. The stream is always consumed in full before the final result is determined.
Result<List<Integer>, List<String>> result = Stream.of(
Result.ok(1),
Result.err("oops"),
Result.ok(3)
)
.collect(ResultCollector.collector());If every element is Ok, the returned result is Ok containing a list of all unwrapped values. If any element is Err, the returned result is Err containing a list of all unwrapped errors. Encounter order is preserved in both cases.
ResultCombiner combines 2–8 independent Result values using a function. All results are evaluated up front; if any are errors, all errors are collected and returned together. The combining function is only invoked when every input is successful.
Result<Address, List<String>> address = ResultCombiner.combine(
Address::new,
parseStreet(input),
parseCity(input),
parsePostalCode(input)
);This is useful for validating multiple independent fields simultaneously and surfacing all failures at once, rather than short-circuiting on the first error as fluent chaining does.
The API follows consistent naming patterns:
| Prefix / name | Meaning |
|---|---|
map |
Transform the value to a new type |
mapError |
Transform the error to a new type |
flatMap |
Transform the value into a new result and flatten (merge) it |
consume |
Run a side effect with the value (returns the same result) |
consumeError |
Run a side effect with the error |
run |
Run a Runnable depending on the result state |
verify |
Validate the value with a function returning VoidResult |
value |
Suffix on OptionalResult methods — only acts when a value is present |
filter |
Checks a Predicate<T> similar to Optional.filter |
ok / err |
Factory methods for construction |
toResult / toVoidResult / toOptionalResult |
Convert between result types |
OptionalResult has two sets of transformation methods. Without the value suffix, the method receives an Optional<T> and operates on all non-error states (present or empty). With the suffix, it only fires when a value is actually present:
// Operates on Optional<T> — runs whether value is present or empty
optionalResult.map(optional -> optional.map(String::toUpperCase));
// Only runs when a value is present — empty passes through unchanged
optionalResult.mapValue(String::toUpperCase);The Result types aims to be like Optional, but there are differences to ensure proper error handling:
- The
ifPresentnaming pattern did not fit well, so it has been renamed toconsume.- Furthermore, there is no
ifPresentOrElsemethod as those can be presented by anconsumefollowed byconsumeError.
- Furthermore, there is no
- There are no
get,orElseororElseThrow(without exception) methods as these will throw away the error without proper handling.- Instead, use either
orElseThrowwith a givenexceptionSupplieror switch over the values.
- Instead, use either
- The
ofis calledokto be more expressive of an error also technically being anof. - There is no
streammethod as it would also throw away the error without proper handling. oris only supported forOptionalResultand notResultas it would throw away the error without proper handling.