Try - Typed Error Handling
Purpose
The Try<T>
type in the Higher-Kinded-J
library represents a computation that might result in a value of type T
(a Success
) or fail with a Throwable
(a Failure
). It serves as a functional alternative to traditional try-catch
blocks for handling exceptions, particularly checked exceptions, within a computation chain. We can think of it as an Either
where the Left
is an Exception
, but also using try-catch blocks behind the scene, so that we don’t have to.
Try Type
Monadic Structure
Key benefits include:
- Explicit Error Handling: Makes it clear from the return type (
Try<T>
) that a computation might fail. - Composability: Allows chaining operations using methods like
map
andflatMap
, where failures are automatically propagated without interrupting the flow with exceptions. - Integration with HKT: Provides HKT simulation (
TryKind
) and type class instances (TryMonad
) to work seamlessly with generic functional abstractions operating overKind<F, A>
. - Error Recovery: Offers methods like
recover
andrecoverWith
to handle failures gracefully within the computation chain.
It implements MonadError<TryKind<?>, Throwable>
, signifying its monadic nature and its ability to handle errors of type `Throwable.
How to Use Try<T>
You can create Try
instances in several ways:
-
Try.of(Supplier)
: Executes aSupplier
and wraps the result inSuccess
or catches any thrownThrowable
(includingError
and checked exceptions) and wraps it inFailure
.import org.higherkindedj.hkt.trymonad.Try; import java.io.FileInputStream; // Success case Try<String> successResult = Try.of(() -> "This will succeed"); // Success("This will succeed") // Failure case (checked exception) Try<FileInputStream> failureResult = Try.of(() -> new FileInputStream("nonexistent.txt")); // Failure(FileNotFoundException) // Failure case (runtime exception) Try<Integer> divisionResult = Try.of(() -> 10 / 0); // Failure(ArithmeticException)
-
Try.success(value)
: Directly creates aSuccess
instance holding the given value (which can be null).Try<String> directSuccess = Try.success("Known value"); Try<String> successNull = Try.success(null);
-
Try.failure(throwable)
: Directly creates aFailure
instance holding the given non-nullThrowable
.Try<String> directFailure = Try.failure(new RuntimeException("Something went wrong"));
isSuccess()
: Returnstrue
if it's aSuccess
.isFailure()
: Returnstrue
if it's aFailure
.
Getting the Value (Use with Caution)
get()
: Returns the value ifSuccess
, otherwise throws the containedThrowable
. Avoid using this directly; preferfold
,map
,flatMap
, or recovery methods.
Applies a function to the value inside a Success
. If the function throws an exception, the result becomes a Failure
. If the original Try
was a Failure
, map
does nothing and returns the original Failure
.
Try<Integer> initialSuccess = Try.success(5);
Try<String> mappedSuccess = initialSuccess.map(value -> "Value: " + value); // Success("Value: 5")
Try<Integer> initialFailure = Try.failure(new RuntimeException("Fail"));
Try<String> mappedFailure = initialFailure.map(value -> "Value: " + value); // Failure(RuntimeException)
Try<Integer> mapThrows = initialSuccess.map(value -> { throw new NullPointerException(); }); // Failure(NullPointerException)
Applies a function that returns another Try
to the value inside a Success
. This is used to sequence operations where each step might fail. Failures are propagated.
Function<Integer, Try<Double>> safeDivide =
value -> (value == 0) ? Try.failure(new ArithmeticException("Div by zero")) : Try.success(10.0 / value);
Try<Integer> inputSuccess = Try.success(2);
Try<Double> result1 = inputSuccess.flatMap(safeDivide); // Success(5.0)
Try<Integer> inputZero = Try.success(0);
Try<Double> result2 = inputZero.flatMap(safeDivide); // Failure(ArithmeticException)
Try<Integer> inputFailure = Try.failure(new RuntimeException("Initial fail"));
Try<Double> result3 = inputFailure.flatMap(safeDivide); // Failure(RuntimeException) - initial failure propagates
Handling Failures (fold
, recover
, recoverWith
)
Safely handles both cases by applying one of two functions.
String message = result2.fold(
successValue -> "Succeeded with " + successValue,
failureThrowable -> "Failed with " + failureThrowable.getMessage()
); // "Failed with Div by zero"
If Failure
, applies a function Throwable -> T
to produce a new Success
value. If the recovery function throws, the result is a Failure
containing that new exception.
Function<Throwable, Double> recoverHandler = throwable -> -1.0;
Try<Double> recovered1 = result2.recover(recoverHandler); // Success(-1.0)
Try<Double> recovered2 = result1.recover(recoverHandler); // Stays Success(5.0)
Similar to recover
, but the recovery function Throwable -> Try<T>
must return a Try
. This allows recovery to potentially result in another Failure
.
Function<Throwable, Try<Double>> recoverWithHandler = throwable ->
(throwable instanceof ArithmeticException) ? Try.success(Double.POSITIVE_INFINITY) : Try.failure(throwable);
Try<Double> recoveredWith1 = result2.recoverWith(recoverWithHandler); // Success(Infinity)
Try<Double> recoveredWith2 = result3.recoverWith(recoverWithHandler); // Failure(RuntimeException) - re-raised
To use Try
with generic code expecting Kind<F, A>
:
- Get Instance:
TryMonad tryMonad = TryMonad.INSTANCE;
- Wrap(Widen): Use
TRY.widen(myTry)
or factories likeTRY.tryOf(() -> ...)
. - Operate: Use
tryMonad.map(...)
,tryMonad.flatMap(...)
,tryMonad.handleErrorWith(...)
etc. - Unwrap(Narrow): Use
TRY.narrow(tryKind)
to get theTry<T>
back.
TryMonad tryMonad = TryMonad.INSTANCE;
Kind<TryKind.Witness, Integer> tryKind1 = TRY.tryOf(() -> 10 / 2); // Success(5) Kind
Kind<TryKind.Witness, Integer> tryKind2 = TRY.tryOf(() -> 10 / 0); // Failure(...) Kind
// Map using Monad instance
Kind<TryKind.Witness, String> mappedKind = tryMonad.map(Object::toString, tryKind1); // Success("5") Kind
// FlatMap using Monad instance
Function<Integer, Kind<TryKind.Witness, Double>> safeDivideKind =
i -> TRY.tryOf(() -> 10.0 / i);
Kind<TryKind.Witness, Double> flatMappedKind = tryMonad.flatMap(safeDivideKind, tryKind1); // Success(2.0) Kind
// Handle error using MonadError instance
Kind<TryKind.Witness, Integer> handledKind = tryMonad.handleErrorWith(
tryKind2, // The Failure Kind
error -> TRY.success(-1) // Recover to Success(-1) Kind
);
// Unwrap
Try<String> mappedTry = TRY.narrow(mappedKind); // Success("5")
Try<Double> flatMappedTry = TRY.narrow(flatMappedKind); // Success(2.0)
Try<Integer> handledTry = TRY.narrow(handledKind); // Success(-1)
System.out.println(mappedTry);
System.out.println(flatMappedTry);
System.out.println(handledTry);