If you are interested in the OOP+FP way of approaching this, there is a specific type, Either<L, R>, where a specialization can be created as Either<L extends Throwable, R> via a .tryCatch() method.
While it is too much to show the entire implementation of Either<L, R> in this answer (a fully documented implementation is located in this Gist), below is a paraphrasing snapshot of what it looks like to reify any descendant of Throwable into a proper capture filter.
To see the implementation details, specifically focus on the pair of tryCatch() methods. This StackOverflow Answer inspired the first of these two methods.
public final class Either<L, R> {
public static <L, R> Either<L, R> left(L value) {
return new Either<>(Optional.of(value), Optional.empty());
}
public static <L, R> Either<L, R> right(R value) {
return new Either<>(Optional.empty(), Optional.of(value));
}
public static <L extends Throwable, R> Either<L, R> tryCatch(
Supplier<R> successSupplier,
Class<L> throwableType
) {
try {
return Either.right(Objects.requireNonNull(successSupplier.get()));
} catch (Throwable throwable) {
if (throwableType.isInstance(throwable)) {
return Either.left((L) throwable);
}
throw throwable;
}
}
public static <R> Either<RuntimeException, R> tryCatch(Supplier<R> successSupplier) {
return tryCatch(successSupplier, RuntimeException.class);
}
private final Optional<L> left;
private final Optional<R> right;
private Either(Optional<L> left, Optional<R> right) {
if (left.isEmpty() == right.isEmpty()) {
throw new IllegalArgumentException("left.isEmpty() must not be equal to right.isEmpty()");
}
this.left = left;
this.right = right;
}
@Override
public boolean equals(Object object) { ... }
@Override
public int hashCode() { ... }
public boolean isLeft() { ... }
public boolean isRight() { ... }
public L getLeft() { ... }
public R getRight() { ... }
public Optional<R> toOptional() { ... }
public <T> Either<L, T> map(Function<? super R, ? extends T> rightFunction) { ... }
public <T> Either<L, T> flatMap(Function<? super R, ? extends Either<L, ? extends T>> rightFunction) { ... }
public <T> Either<T, R> mapLeft(Function<? super L, ? extends T> leftFunction) { ... }
public <T> Either<L, T> mapRight(Function<? super R, ? extends T> rightFunction) { ... }
public <T> Either<T, R> flatMapLeft(Function<? super L, ? extends Either<? extends T, R>> leftFunction) { ... }
public <T> Either<L, T> flatMapRight(Function<? super R, ? extends Either<L, ? extends T>> rightFunction) { ... }
public <T> T converge(Function<? super L, ? extends T> leftFunction, Function<? super R, ? extends T> rightFunction) { ... }
public <T> T converge() { ... }
public static <T> T converge(Either<? extends T, ? extends T> either) { ... }
public void forEach(Consumer<? super L> leftAction, Consumer<? super R> rightAction) { ... }
}
A quick way to see the above code in action is to check out this validation suite of JUnit tests.
try/catchstatement into my implementation of anEither<L, R>via the specialization of,Either<L extends Throwable, R>, returned via either of the pair oftryCatch()methods: stackoverflow.com/a/79303726/501113