Basic Usage

How to solve simple use-case scenarios

In this section, we’ll cover foundational use cases, including checking the status of a result, unwrapping the value inside a result, and taking different actions based on success or failure.

No need for if blocks or early return statements when you can handle success and failure without any hassle.
Figure 12. No need for if blocks or early return statements when you can handle success and failure without any hassle.

These basics will help you handle errors more cleanly and efficiently without cluttering your code with try-catch blocks.

Checking Success or Failure

How to find out if the operation succeded or failed

As we discovered earlier, we can easily determine if a given Result instance is successful or not.

Checking Success

We can use hasSuccess to obtain a boolean value that represents whether a result is successful.

Figure 13. Checking success
1 @Test
2 void testHasSuccess() {
3   // When
4   boolean result1HasSuccess = success(1024).hasSuccess();
5   boolean result2HasSuccess = failure(1024).hasSuccess();
6   // Then
7   assertTrue(result1HasSuccess);
8   assertFalse(result2HasSuccess);
9 }

Checking Failure

We can also use hasFailure to find out if a result contains a failure value.

Figure 14. Checking failure
1 @Test
2 void testHasFailure() {
3   // When
4   boolean result1HasFailure = success(512).hasFailure();
5   boolean result2HasFailure = failure(512).hasFailure();
6   // Then
7   assertFalse(result1HasFailure);
8   assertTrue(result2HasFailure);
9 }

Conclusion

We discussed how to determine the state of a Result object using hasSuccess and hasFailure. These methods provide a straightforward way to identify the outcome of an operation, helping you make decisions based on the outcome.

Unwrapping Values

How to get values out of Result objects

In essence, a Result object is just a container that wraps a success or a failure value for us. Therefore, sometimes you are going to want to get that value out of the container.

As useful as this may seem, we will soon realize that we won’t be doing it very often.

Unwrapping Success

The most basic way to retrieve the success value wrapped inside a result is by using getSuccess. This method will return an optional success value, depending on whether the result was actually successful or not.

Figure 15. Unwrapping success
 1 @Test
 2 void testGetSuccess() {
 3   // Given
 4   Result<?, ?> result1 = success("SUCCESS");
 5   Result<?, ?> result2 = failure("FAILURE");
 6   // Then
 7   Optional<?> success1 = result1.getSuccess();
 8   Optional<?> success2 = result2.getSuccess();
 9   // Then
10   assertEquals("SUCCESS", success1.get());
11   assertTrue(success2::isEmpty);
12 }

Unwrapping Failure

Similarly, we can use getFailure to obtain the failure value held by a Result object.

Figure 16. Unwrapping failure
 1 @Test
 2 void testGetFailure() {
 3   // Given
 4   Result<?, ?> result1 = success("SUCCESS");
 5   Result<?, ?> result2 = failure("FAILURE");
 6   // Then
 7   Optional<?> failure1 = result1.getFailure();
 8   Optional<?> failure2 = result2.getFailure();
 9   // Then
10   assertTrue(failure1::isEmpty);
11   assertEquals("FAILURE", failure2.get());
12 }

Unlike Optional’s get, these methods are null-safe. However, in practice, we will not be using them frequently. Especially, since there are more convenient ways to get the success value out of a result.

Using Alternative Success

We can use orElse to provide an alternative success value that must be returned when the result is unsuccessful.

Figure 17. Using alternative success
 1 @Test
 2 void testGetOrElse() {
 3   // Given
 4   Result<String, String> result1 = success("IDEAL");
 5   Result<String, String> result2 = failure("ERROR");
 6   String alternative = "OTHER";
 7   // When
 8   String value1 = result1.orElse(alternative);
 9   String value2 = result2.orElse(alternative);
10   // Then
11   assertEquals("IDEAL", value1);
12   assertEquals("OTHER", value2);
13 }

Note that alternative success values can be null.

Mapping Failure

The orElseMap method is similar to Optional’s orElseGet, but it takes a mapping Function instead of a Supplier. The function will receive the failure value to produce the alternative success value.

Figure 18. Mapping failure
 1 @Test
 2 void testGetOrElseMap() {
 3   // Given
 4   Result<String, Integer> result1 = success("OK");
 5   Result<String, Integer> result2 = failure(1024);
 6   Result<String, Integer> result3 = failure(-256);
 7   Function<Integer, String> mapper = x -> x > 0 ? "HI" : "LO";
 8   // When
 9   String value1 = result1.orElseMap(mapper);
10   String value2 = result2.orElseMap(mapper);
11   String value3 = result3.orElseMap(mapper);
12   // Then
13   assertEquals("OK", value1);
14   assertEquals("HI", value2);
15   assertEquals("LO", value3);
16 }

Although probably not the best practice, the mapping function may return null.

Streaming Success or Failure

Finally, we can use streamSuccess and streamFailure to wrap the value held by an instance of Result into a possibly-empty Stream object.

Figure 19. Streaming success or failure
 1 @Test
 2 void testStreamSuccess() {
 3   // Given
 4   Result<?, ?> result1 = success("Yes");
 5   Result<?, ?> result2 = failure("No");
 6   // When
 7   Stream<?> stream1 = result1.streamSuccess();
 8   Stream<?> stream2 = result2.streamSuccess();
 9   // Then
10   assertEquals("Yes", stream1.findFirst().orElse(null));
11   assertNull(stream2.findFirst().orElse(null));
12 }
13 
14 @Test
15 void testStreamFailure() {
16   // Given
17   Result<?, ?> result1 = success("Yes");
18   Result<?, ?> result2 = failure("No");
19   // When
20   Stream<?> stream1 = result1.streamFailure();
21   Stream<?> stream2 = result2.streamFailure();
22   // Then
23   assertNull(stream1.findFirst().orElse(null));
24   assertEquals("No", stream2.findFirst().orElse(null));
25 }

Conclusion

We explored various ways to retrieve values from results. Using these methods you can efficiently access the underlying data within a Result object, whether it’s a success or a failure.

Conditional Actions

How to handle success and failure scenarios

We’ll now delve into a set of methods that allow you to take conditional actions based on the state of a result. They provide a cleaner and more expressive way to handle success and failure scenarios, eliminating the need for lengthy if/else blocks.

Handling Success

We can use ifSuccess to specify an action that must be executed if the result represents a successful outcome. This method takes a consumer function that will be applied to the success value wrapped by the result.

Figure 20. Handling success
 1 @Test
 2 void testIfSuccess() {
 3   // Given
 4   List<Object> list = new ArrayList<>();
 5   Result<Integer, String> result = success(100);
 6   // When
 7   result.ifSuccess(list::add);
 8   // Then
 9   assertEquals(100, list.getFirst());
10 }

In this example, ifSuccess ensures that the provided action (adding the success value to the list) is only executed if the parsing operation is successful.

Handling Failure

On the other hand, we can use ifFailure method to define an action that must be taken when the result represents a failure. This method also takes a Consumer that will be applied to the failure value inside the result.

Figure 21. Handling failure
 1 @Test
 2 void testIfFailure() {
 3   // Given
 4   List<Object> list = new ArrayList<>();
 5   Result<Integer, String> result = failure("ERROR");
 6   // When
 7   result.ifFailure(list::add);
 8   // Then
 9   assertEquals("ERROR", list.getFirst());
10 }

Here, ifFailure ensures that the provided action (adding the failure value to the list) is only executed if the parsing operation fails.

Handling Both Scenarios

Finally, ifSuccessOrElse allows you to specify two separate actions: one for when the operation succeeded and another for when it failed. This method takes two consumer functions: the first for handling the success case and the second for handling the failure case.

Figure 22. Handling both scenarios
 1 @Test
 2 void testIfSuccessOrElse() {
 3   // Given
 4   List<Object> list1 = new ArrayList<>();
 5   List<Object> list2 = new ArrayList<>();
 6   Result<Long, String> result1 = success(100L);
 7   Result<Long, String> result2 = failure("ERROR");
 8   // When
 9   result1.ifSuccessOrElse(list1::add, list1::add);
10   result2.ifSuccessOrElse(list2::add, list2::add);
11   // Then
12   assertEquals(100L, list1.getFirst());
13   assertEquals("ERROR", list2.getFirst());
14 }

In this example, ifSuccessOrElse simplifies conditional logic by providing a single method to handle both success and failure scenarios, making the code more concise and readable.

Conclusion

We explained how to handle success and failure scenarios using these three methods. They provide a powerful way to perform conditional actions based on the state of a Result, streamlining your error handling and making your code more readable and maintainable.