<dependency>
<groupId>dev.pablolamtenzan</groupId>
<artifactId>error-or</artifactId>
<version>1.1.1</version>
</dependency>
implementation 'dev.pablolamtenzan:error-or:1.1.1'
- Star the Project β
- Overview
- Installation
- Creating an ErrorOr instance
- Properties
- Methods
match
matchAsync
matchFirst
matchFirstAsync
consume
consumeAsync
consumeFirst
consumeFirstAsync
getOr
getOrAsync
ifError
getOrThrow
map
mapAsync
mapError
mapErrorAsync
or
orAsync
orError
orErrorAsync
orErrors
orErrorsAsync
onValue
onValueAsync
onError
onErrorAsync
failIf
failIfAsync
- Error Types
- Organizing Errors
- Contribution π€²
- Credits π
- License πͺͺ
Found this project helpful? Show your appreciation by giving us a star!
ErrorOr is a Java implementation of the discriminated union pattern, providing a fluent and expressive way to handle operations that can either result in a value or an error. Inspired by the original ErrorOr project in C# by Amichai Mantinband, this library aims to bring similar functionality to the Java ecosystem.
- Result Handling: Seamlessly handle operations that may result in a value or an error.
- Error Aggregation: Collect multiple errors during a single operation.
- Functional Methods: Use a variety of methods to operate on results and errors in a functional style.
- Fluent API: Chain methods together to build complex operations in a readable way.
- Simple API: Easy to understand and use API for error and result handling.
- Flexible: Supports multiple error types and custom error definitions.
- Integrated: Works well with existing Java code and libraries.
- Lightweight: Minimal dependencies and overhead.
Instead of relying on traditional exception handling, ErrorOr
allows you to represent the result of an operation as either a success or an error. This approach makes your code cleaner and more predictable.
Traditional Approach:
public int divide(int numerator, int denominator) {
if (denominator == 0) {
throw new IllegalArgumentException("Cannot divide by zero");
}
return numerator / denominator;
}
try {
int result = divide(10, 2);
System.out.println(result);
} catch (IllegalArgumentException e) {
System.err.println(e.getMessage());
}
With ErrorOr<T>
:
public ErrorOr<Integer> divide(int numerator, int denominator) {
if (denominator == 0) {
return ErrorOr.ofError(MathErrors.DIVISION_BY_ZERO);
}
return ErrorOr.of(numerator / denominator);
}
ErrorOr<Integer> result = divide(10, 2);
result.consume(
errors -> errors.forEach(error -> System.err.println(error.description())),
value -> System.out.println(value)
);
ErrorOr
can encapsulate multiple errors, providing a comprehensive way to handle all potential issues in a single operation.
public static ErrorOr<User> createUser(String name, int age) {
List<Error> errors = new ArrayList<>();
if (name == null || name.isBlank()) {
errors.add(Error.validation("USER.INVALID_NAME", "Name cannot be blank"));
}
if (age < 0) {
errors.add(Error.validation("USER.INVALID_AGE", "Age cannot be negative"));
}
if (!errors.isEmpty()) {
return ErrorOr.ofError(errors);
}
return ErrorOr.of(new User(name, age));
}
ErrorOr<User> userResult = createUser("", -1);
userResult.consume(
errors -> errors.forEach(error -> System.err.println(error.description())),
user -> System.out.println("User created: " + user)
);
ErrorOr
provides a rich set of methods to operate on the results and errors in a functional and fluent manner.
Transform the value if present, otherwise return the current instance:
ErrorOr<String> result = divide(10, 2).map(value -> "Result: " + value);
CompletableFuture<ErrorOr<String>> asyncResult = divide(10, 2).mapAsync(value -> CompletableFuture.completedFuture("Result: " + value));
Apply functions to handle both success and error cases:
String message = divide(10, 2).match(
errors -> "Errors: " + errors.size(),
value -> "Value: " + value
);
CompletableFuture<String> asyncMessage = divide(10, 2).matchAsync(
errors -> CompletableFuture.completedFuture("Errors: " + errors.size()),
value -> CompletableFuture.completedFuture("Value: " + value)
);
Consume the value or errors using the provided consumers:
divide(10, 2).consume(
errors -> System.err.println("Errors: " + errors),
value -> System.out.println("Value: " + value)
);
CompletableFuture<Void> asyncConsume = divide(10, 2).consumeAsync(
errors -> CompletableFuture.runAsync(() -> System.err.println("Errors: " + errors)),
value -> CompletableFuture.runAsync(() -> System.out.println("Value: " + value))
);
Provide an alternative value or ErrorOr
if the current instance contains an error:
ErrorOr<Integer> alternativeResult = divide(10, 0).or(ErrorOr.of(0));
CompletableFuture<ErrorOr<Integer>> asyncAlternativeResult = divide(10, 0).orAsync(() -> CompletableFuture.completedFuture(0));
These methods, among others, provide a robust and flexible approach to error handling and result management, promoting clean and maintainable code. By using ErrorOr
, you can handle complex error scenarios gracefully without resorting to exception-based control flow.
Add the following dependency to your pom.xml
:
<dependency>
<groupId>dev.pablolamtenzan</groupId>
<artifactId>error-or</artifactId>
<version>1.1.1</version>
</dependency>
Add the following dependency to your build.gradle
:
implementation 'dev.pablolamtenzan:error-or:1.1.1'
For verifying the integrity of the project, you can use the PGP public key provided. The public key is now available on a key server. You can retrieve the public key using the following command:
gpg --keyserver keyserver.ubuntu.com --search-keys [email protected]
Alternatively, you can find the key in the PGP-PUBLIC-KEY file in the repository.
The ErrorOr
class provides multiple ways to create instances representing either a successful value or one or more errors. Below are the different methods available for creating ErrorOr
instances.
The of
factory method is used to create an ErrorOr
instance that holds a successful value.
ErrorOr<Integer> successResult = ErrorOr.of(5);
The of
factory method can also be used to create an ErrorOr
instance by copying another ErrorOr
instance.
ErrorOr<String> stringSuccess = ErrorOr.of("Success");
ErrorOr<String> anotherStringSuccess = ErrorOr.of(stringSuccess);
The ofError
factory method is used to create an ErrorOr
instance that holds one or more errors.
This method has several overloads:
- Single Error
Error validationError = Error.validation("Invalid Input", "The provided input is invalid.");
ErrorOr<Integer> errorResult = ErrorOr.ofError(validationError);
- Multiple Errors (varargs)
Error validationError = Error.validation("Invalid Input", "The provided input is invalid.");
Error conflictError = Error.conflict("Conflict Error", "A conflict occurred.");
ErrorOr<Integer> errorResult = ErrorOr.ofError(validationError, conflictError);
- Iterable of Errors
List<Error> errors = List.of(
Error.validation("Invalid Input", "The provided input is invalid."),
Error.conflict("Conflict Error", "A conflict occurred.")
);
ErrorOr<Integer> errorResult = ErrorOr.ofError(errors);
- Other ErrorOr instance
ErrorOr<String> stringErrorOr = ErrorOr.ofError(Error.validation("Invalid Input", "The provided input is invalid."));
ErrorOr<Integer> integerErrorOr = ErrorOr.ofError(stringErrorOr);
Indicates whether the ErrorOr
object contains an error.
ErrorOr<Integer> result = User.create();
if (result.isError()) {
// the result contains one or more errors
}
Returns the value contained in the ErrorOr
object. Not available if the object contains an error.
ErrorOr<Integer> result = User.create();
if (!result.isError()) {
System.out.println(result.value());
}
Returns a list of errors contained in the ErrorOr
object. Not available if the object contains a valid value.
ErrorOr<Integer> result = User.create();
if (result.isError()) {
result.errors().forEach(error -> System.out.println(error.description()));
}
Returns the first error in the list of errors contained in the ErrorOr
object. Not available if the object contains a valid value.
ErrorOr<Integer> result = User.create();
if (result.isError()) {
Error firstError = result.firstError();
System.out.println(firstError.description());
}
Applies one of two functions depending on whether the ErrorOr
instance is a value or an error.
String message = result.match(
errors -> "Errors: " + errors.size(),
value -> "Value: " + value
);
Asynchronously applies one of two functions depending on whether the ErrorOr
instance is a value or an error.
CompletableFuture<String> message = result.matchAsync(
errors -> CompletableFuture.completedFuture("Errors: " + errors.size()),
value -> CompletableFuture.completedFuture("Value: " + value)
);
Applies one of two functions depending on whether the ErrorOr
instance is a value or an error, using the first error if multiple errors are present.
String message = result.matchFirst(
error -> "Error: " + error.description(),
value -> "Value: " + value
);
Asynchronously applies one of two functions depending on whether the ErrorOr
instance is a value or an error, using the first error if multiple errors are present.
CompletableFuture<String> message = result.matchFirstAsync(
error -> CompletableFuture.completedFuture("Error: " + error.description()),
value -> CompletableFuture.completedFuture("Value: " + value)
);
Consumes the value or errors, depending on the state of the ErrorOr
instance.
result.consume(
errors -> System.out.println("Errors: " + errors),
value -> System.out.println("Value: " + value)
);
Asynchronously consumes the value or errors, depending on the state of the ErrorOr
instance.
CompletableFuture<Void> consumption = result.consumeAsync(
errors -> CompletableFuture.runAsync(() -> System.out.println("Errors: " + errors)),
value -> CompletableFuture.runAsync(() -> System.out.println("Value: " + value))
);
Consumes the value or the first error, depending on the state of the ErrorOr
instance.
result.consumeFirst(
error -> System.out.println("Error: " + error.description()),
value -> System.out.println("Value: " + value)
);
Asynchronously consumes the value or the first error, depending on the state of the ErrorOr
instance.
CompletableFuture<Void> consumption = result.consumeFirstAsync(
error -> CompletableFuture.runAsync(() -> System.out.println("Error: " + error.description())),
value -> CompletableFuture.runAsync(() -> System.out.println("Value: " + value))
);
Returns the value if present, otherwise applies the provided function to the errors and returns the result.
Integer value = result.getOr(errors -> -1);
Integer value = result.getOr(() -> -1);
Asynchronously returns the value if present, otherwise applies the provided function to the errors and returns the result.
CompletableFuture<Integer> value = result.getOrAsync(errors -> CompletableFuture.completedFuture(-1));
CompletableFuture<Integer> value = result.getOrAsync(() -> CompletableFuture.completedFuture(-1));
Performs the given action if the ErrorOr
instance contains an error.
result.ifError(errors -> System.out.println("Errors: " + errors));
result.ifError(() -> System.out.println("An error occurred"));
Returns the value if present, otherwise applies the provided function to the errors and throws the resulting exception.
Integer value = result.getOrThrow(errors -> new RuntimeException("Error occurred"));
Integer value = result.getOrThrow(() -> new RuntimeException("Error occurred"));
Applies the given function to the value if present, otherwise returns the current ErrorOr
instance.
ErrorOr<String> mappedResult = result.map(value -> "Value: " + value);
Asynchronously applies the given function to the value if present, otherwise returns the current ErrorOr
instance.
CompletableFuture<ErrorOr<String>> mappedResult = result.mapAsync(value -> CompletableFuture.completedFuture("Value: " + value));
Applies the given function to each error if present, otherwise returns the current ErrorOr
instance.
ErrorOr<Integer> mappedResult = result.mapError(error -> Error.unexpected("MappedError", "Mapped error description"));
Asynchronously applies the given function to each error if present, otherwise returns the current ErrorOr
instance.
CompletableFuture<ErrorOr<Integer>> mappedResult = result.mapErrorAsync(error -> CompletableFuture.completedFuture(Error.unexpected("MappedError", "Mapped error description")));
Returns this instance if it is not an error, otherwise returns the provided alternative.
ErrorOr<Integer> alternativeResult = result.or(ErrorOr.of(0));
ErrorOr<Integer> alternativeResult = result.or(errors -> 0);
ErrorOr<Integer> alternativeResult = result.or(() -> 0);
Asynchronously returns this instance if it is not an error, otherwise returns the provided alternative.
CompletableFuture<ErrorOr<Integer>> alternativeResult = result.orAsync(errors -> CompletableFuture.completedFuture(0));
CompletableFuture<ErrorOr<Integer>> alternativeResult = result.orAsync(() -> CompletableFuture.completedFuture(0));
Returns this instance if it is not an error, otherwise returns the provided error alternative.
ErrorOr<Integer> alternativeResult = result.orError(errors -> Error.unexpected());
ErrorOr<Integer> alternativeResult = result.orError(() -> Error.unexpected());
Asynchronously returns this instance if it is not an error, otherwise returns the provided error alternative.
CompletableFuture<ErrorOr<Integer>> alternativeResult = result.orErrorAsync(errors -> CompletableFuture.completedFuture(Error.unexpected()));
CompletableFuture<ErrorOr<Integer>> alternativeResult = result.orErrorAsync(() -> CompletableFuture.completedFuture(Error.unexpected()));
Returns this instance if it is not an error, otherwise returns the provided list of errors.
ErrorOr<Integer> alternativeResult = result.orErrors(errors -> List.of(Error.unexpected()));
ErrorOr<Integer> alternativeResult = result.orErrors(() -> List.of(Error.unexpected()));
Asynchronously returns this instance if it is not an error, otherwise returns the provided list of errors.
CompletableFuture<ErrorOr<Integer>> alternativeResult = result.orErrorsAsync(errors -> CompletableFuture.completedFuture(List.of(Error.unexpected())));
CompletableFuture<ErrorOr<Integer>> alternativeResult = result.orErrorsAsync(() -> CompletableFuture.completedFuture(List.of(Error.unexpected())));
Performs the given action if this instance is a value.
result.onValue(value -> System.out.println("Value: " + value));
Asynchronously performs the given action if this instance is a value.
CompletableFuture<ErrorOr<Integer>> valueAction = result.onValueAsync(value -> CompletableFuture.runAsync(() -> System.out.println("Value: " + value)));
Performs the given action if this instance is an error.
result.onError(errors -> System.out.println("Errors: " + errors));
Asynchronously performs the given action if this instance is an error.
CompletableFuture<ErrorOr<Integer>> errorAction = result.onErrorAsync(errors -> CompletableFuture.runAsync(() -> System.out.println("Errors: " + errors)));
Returns this instance if it is not an error, otherwise applies the given predicate to the value and returns a new ErrorOr
instance containing the provided errors if the predicate is satisfied.
ErrorOr<Integer> checkedResult = result.failIf(value -> value < 0, Error.validation("NegativeValue", "Value cannot be negative"));
ErrorOr<Integer> checkedResult = result.failIf(value -> value < 0, List.of(Error.validation("NegativeValue", "Value cannot be negative")));
Asynchronously returns this instance if it is not an error, otherwise applies the given predicate to the value and returns a new ErrorOr
instance containing the provided errors if the predicate is satisfied.
CompletableFuture<ErrorOr<Integer>> checkedResult = result.failIfAsync(
value -> CompletableFuture.completedFuture(value < 0),
Error.validation("NegativeValue", "Value cannot be negative")
);
CompletableFuture<ErrorOr<Integer>> checkedResult = result.failIfAsync(
value -> CompletableFuture.completedFuture(value < 0),
List.of(Error.validation("NegativeValue", "Value cannot be negative"))
);
Error handling in ErrorOr
is facilitated through the use of the Error
class, which supports various built-in error types and custom error definitions.
The ErrorType
class defines several standard error types that can be used throughout your application. These built-in error types include:
- Failure
- Unexpected
- Validation
- Conflict
- Not Found
- Unauthorized
- Forbidden
These types can be instantiated using the Error
factory methods. Below is an example of the failure
error type, showcasing the various overloads available. Other error types follow the same pattern.
// Failure
Error failureError = Error.failure("FAILURE_CODE", "A failure has occurred.");
Error failureErrorWithMetadata = Error.failure("FAILURE_CODE", "A failure has occurred.", metadata);
Error generalFailureError = Error.failure();
Error generalFailureErrorWithMetadata = Error.failure(metadata);
Each of these types can be instantiated using the Error
factory methods:
Error failureError = Error.failure("FAILURE_CODE", "A failure has occurred.");
Error unexpectedError = Error.unexpected("UNEXPECTED_CODE", "An unexpected error has occurred.");
Error validationError = Error.validation("VALIDATION_CODE", "A validation error has occurred.");
Error conflictError = Error.conflict("CONFLICT_CODE", "A conflict has occurred.");
Error notFoundError = Error.notFound("NOT_FOUND_CODE", "A 'Not Found' error has occurred.");
Error unauthorizedError = Error.unauthorized("UNAUTHORIZED_CODE", "An 'Unauthorized' error has occurred.");
Error forbiddenError = Error.forbidden("FORBIDDEN_CODE", "A 'Forbidden' error has occurred.");
Each method has overloads to include metadata if necessary:
Map<String, Object> metadata = Map.of("key", "value");
Error errorWithMetadata = Error.validation("VALIDATION_CODE", "A validation error has occurred.", metadata);
If the built-in error types do not suit your needs, you can define custom error types. Custom error types can be created using the custom
method:
int customTypeOrdinal = 99;
Error customError = Error.custom(customTypeOrdinal, "CUSTOM_CODE", "A custom error has occurred.");
Error customErrorWithMetadata = Error.custom(customTypeOrdinal, "CUSTOM_CODE", "A custom error has occurred.", metadata);
A good practice is to organize your errors in a static class for better manageability and readability.
For instance:
public class MathErrors {
public static final Error DIVISION_BY_ZERO = Error.failure("MATH.DIVISION_BY_ZERO", "Division by zero is not allowed.");
public static final Error NEGATIVE_NUMBER = Error.validation("MATH.NEGATIVE_NUMBER", "Negative numbers are not allowed.");
}
You can then use these predefined errors in your methods:
public static ErrorOr<Integer> divide(int numerator, int denominator) {
if (denominator == 0) {
return ErrorOr.ofError(MathErrors.DIVISION_BY_ZERO);
}
return ErrorOr.of(numerator / denominator);
}
We welcome contributions from the community. If you have any questions, comments, or suggestions, please open an issue or create a pull request. Your feedback is greatly appreciated.
This project was inspired by the original ErrorOr project in C# by Amichai Mantinband. All credit for the concept and original implementation goes to him. This Java implementation aims to extend his work and bring the same functionality to Java developers.
This project is licensed under the terms of the MIT license.