The project provides Result-type similar to Result-type in Rust that allows to return either successful result or otherwise some kind of error.
In Java, the native way of reporting errors are exceptions, either checked or unchecked. You do not need Result-type most of the time in Java-code, where you can directly throw exceptions. But there are situations, where more functional-style is used. In such situations pure-functions are expected that throw no exceptions. Handling exception in such situations can be cumbersome and require a lot of boilerplate code. Result-type and associated helper-classes help with exception handling and allow to write idiomatic functional code that can interact with methods that throw exceptions.
Result-type provides a way to pass error information as a first-class value through the code written in functional style. Routines are provided for interoperability of normal code that uses exception and functional code that uses Result-type, so that exceptions can be caught and propagated as errors in Result-type and then rethrown again later in the control-flow.
Java 21 is required.
result4j is available in Maven Central.
<dependency>
<groupId>com.github.sviperll</groupId>
<artifactId>result4j</artifactId>
<version>$LATEST_VERSION<!-- see above --></version>
</dependency>
- Small well-defined library that requires minimal maintenance
- Work with the broad range of mainstream Java code (not tailored to some niche flavor of Java)
- No bloat
- Zero or minimal dependencies
- Work with Java-modules (JPMS) and modern Java
API Documentation is available for reference.
Result type can be either a successful result or some kind of error.
Result<String, E> suc = Result.success("Hello, World!");
The above line declares successful result value.
Result<String, Integer> err = Result.error(404);
The above line declares error-value.
Result<String, Integer> result = ...;
switch (result) {
case Result.Success<String, Integer>(String value) -> System.out.println(value);
case Result.Error<String, Integer>(Integer code) ->
throw new IOException("%s: error".formatted(code));
}
Pattern matching can be used to check unknown result value as shown above.
Result<String, Integer> receivedResult = ...;
String value = receivedResult.orOnErrorThrow(code -> new IOException("%s: error".formatted(code)));
System.out.println(value);
Instead of a low-level pattern-matching,
higher level helper-methods are available in Result
-class.
In the snippet above orOnErrorThrow
is used to throw exception when Result
contains error.
Result-type is created for interoperability between normal Java-code that throws exception and more functional code.
Catcher.ForFunctions<IOException> io =
Catcher.of(IOException.class).forFunctions();
String concatenation =
Stream.of("a.txt", "b.txt", "c.txt")
.map(io.catching(name -> loadResource(name)))
.collect(ResultCollectors.toSingleResult(Collectors.join()))
.orOnErrorThrow(Function.identity());
Above code uses Catcher
class to adapt functions that
throw exceptions to return Result-type instead.
ResultCollectors
class contains helper-methods to collect multiple Result
s into a single one.
You do not need Catcher
class in normal Java-code, where you can directly throw exceptions.
But the above snippet can serve as an example of situations, where more functional-style is used.
In such situations pure-functions are expected that throw no exceptions.
Handling exception in such situations can be cumbersome and require a lot of boilerplate code.
class MyMain {
String loadResource(String name) throws IOException {
// ...
}
public static void main(String[] args) throws IOException {
Catcher.ForFunctions<IOException> io =
Catcher.of(IOException.class).forFunctions();
Function<String, Result<String, IOException>> f = io.catching(MyMain::loadResult);
Result<String, IOException> result = f.apply("my-resource");
String value = result.orOnErrorThrow(Function.identity());
System.out.println(value);
}
}
The example above shows the usage of the catching
method of the Catcher
class, that
allows to adapt exception throwing method and instead to have a method that returns Result
with
exception representing as an error-value.
There is also an AdaptingCatcher
class that allows to adapt or wrap exceptions.
AdaptingCatcher.ForFunctions<IOException, PipelineException> io =
Catcher.of(IOException.class).map(PipelineException::new).forFunctions();
AdaptingCatcher.ForFunctions<MLException, PipelineException> ml =
Catcher.of(MLException.class).map(PipelineException::new).forFunctions();
List<Animal> animals1 =
List.of("cat.jpg", "dog.jpg")
.stream()
.map(io.catching(Fakes::readFile))
.map(Result.flatMapping(ml.catching(Fakes::recognizeImage)))
.collect(ResultCollectors.toSingleResult(Collectors.toList()))
.orOnErrorThrow(Function.identity());
ResultAssert.assertThat(animals)
.isSuccess()
.hasSuccessValueThat()
.asInstanceOf(list(Animal.class))
.containsExactlyInAnyOrderElementsOf(List.of(Animal.CAT, Animal.DOG));
Result4j now provides integration with the AssertJ testing-framework.
To use it you should use a separate assertj-result4j
artifact
(See Maven Central Page):
<dependency>
<groupId>com.github.sviperll</groupId>
<artifactId>assertj-result4j</artifactId>
<version>$LATEST_VERSION<!-- see above --></version>
</dependency>
The library allows you to write easy to read assertions in the style of AsserJ.
An assertion of a result of a successful operation:
assertThat(result)
.isSuccess()
.hasSuccessValueThat()
.asInstanceOf(list(Integer.class))
.containsExactlyInAnyOrderElementsOf(List.of(456, 234, 123));
An assertion of an error:
assertThat(result)
.isError()
.hasErrorThat()
.asInstanceOf(InstanceOfAssertFactories.THROWABLE)
.isExactlyInstanceOf(RuntimeException.class)
.cause()
.isExactlyInstanceOf(NumberFormatException.class)
.hasMessage("For input string: \"xvxv\"");
See the API documentation for more details.