Skip to content

feature: #98 | Ability to recover from specific exceptions#100

Merged
ShivamNagpal merged 1 commit intoZeplinko:developfrom
a-anand-91119:main
Nov 12, 2025
Merged

feature: #98 | Ability to recover from specific exceptions#100
ShivamNagpal merged 1 commit intoZeplinko:developfrom
a-anand-91119:main

Conversation

@a-anand-91119
Copy link
Copy Markdown
Contributor

@a-anand-91119 a-anand-91119 commented Nov 7, 2025

@ShivamNagpal What you are about to read is entirely from LLM. I wasted some time reading this to make sure it correct wrt to the change. You also spend some time reading this 😉

Thoughts on this? Really need this exception specific recovered and handlers.


Add Exception-Type Filtering to recover() and onFailure() Methods

Summary

This PR enhances the Try class with exception-type filtering capabilities for both recovery and error handling operations. Users can now handle multiple exception types with a single handler, making error handling more concise and maintainable.

Changes

1. Varargs recover() Method

Added a new recover method that accepts multiple exception types:

@SafeVarargs
public final <E extends Exception> Try<T> recover(
    Function<? super Exception, ? extends Try<T>> failureMapper,
    Class<? extends E>... exceptionTypes
)

Example Usage:

Try.to(() -> readFile())
    .recover(
        ex -> Try.success("default content"),
        IOException.class,
        FileNotFoundException.class,
        SocketTimeoutException.class
    );

2. Type-Specific onFailure() Methods

Added two new onFailure overloads for conditional side effects:

Single Exception Type:

public <E extends Exception> Try<T> onFailure(
    Class<? extends E> exceptionType,
    Consumer<? super Exception> errorConsumer
)

Multiple Exception Types:

@SafeVarargs
public final <E extends Exception> Try<T> onFailure(
    Consumer<? super Exception> errorConsumer,
    Class<? extends E>... exceptionTypes
)

Example Usage:

Try.to(() -> fetchData())
    .onFailure(IOException.class, ex -> logger.error("I/O error: {}", ex.getMessage()))
    .onFailure(TimeoutException.class, ex -> logger.warn("Timeout: {}", ex.getMessage()))
    .onFailure(ex -> logger.error("Unexpected error", ex));

Design Decisions

Parameter Ordering

The methods use different parameter orders to avoid method overloading ambiguity:

Method Signature Reasoning
Single exception recover recover(Class, Function) Exception type first reads naturally
Varargs recover recover(Function, Class...) Function first prevents ambiguity
Single exception onFailure onFailure(Class, Consumer) Exception type first for consistency
Varargs onFailure onFailure(Consumer, Class...) Consumer first prevents ambiguity

This design prevents Java compiler ambiguity while optimizing ergonomics for the most common case (single exception type).

Why Varargs for recover?

Before:

Try.to(() -> operation())
    .recover(IOException.class, ex -> Try.success("default"))
    .recover(TimeoutException.class, ex -> Try.success("default"))
    .recover(SocketException.class, ex -> Try.success("default"));

After:

Try.to(() -> operation())
    .recover(
        ex -> Try.success("default"),
        IOException.class,
        TimeoutException.class,
        SocketException.class
    );

This eliminates repetitive chaining when multiple exception types need the same recovery logic.

Why Type-Specific onFailure?

The onFailure methods are for side effects (logging, metrics, cleanup), not transformation. Type filtering enables:

  1. Differentiated logging levels based on exception type
  2. Type-specific metrics tracking
  3. Conditional cleanup operations
  4. Chainable error observability without modifying the Try

Alternatives Considered

1. Predicate-Based onHandle()

Rejected approach:

  public Try<T> onHandle(
      Predicate<Try<T>> condition,
      Consumer<? super T> successConsumer,
      Consumer<? super Exception> failureConsumer
  )

Why rejected:

  • compose() needs predicates for transformation logic, but onHandle() is purely for side effects
  • Exception type filtering covers 95% of conditional side-effect use cases
  • Would lead to awkward empty consumer parameters
  • Adds API complexity without sufficient value

Users needing custom conditions can use the existing single-consumer onHandle():

  .onHandle(aTry -> {
      if (customCondition(aTry)) {
          handleIt(aTry);
      }
  })

2. Removing Single Exception recover() Method

Rejected approach: Only keep the varargs version

Why rejected:

  • Single exception recovery is the most common case (~80-90%)
  • Having Class parameter first reads more naturally: recover(IOException.class, handler)
  • Varargs forces function-first ordering to avoid ambiguity
  • Both methods serve different ergonomic needs

3. Reverse Parameter Order in Single Exception recover()

Rejected approach: Make single exception match varargs order: recover(Function, Class)

Why rejected:

  • Would cause method overloading ambiguity with varargs version
  • Java cannot distinguish recover(Function, Class) from recover(Function, Class...)
  • Code would not compile

Breaking Changes

None. All changes are additive - existing API remains unchanged.

Questions for Reviewers

  1. API Design: Do the different parameter orders between single and varargs versions feel intuitive, or would you prefer a different approach?
  2. Method Naming: Should the varargs versions have different names (e.g., recoverAny(), onFailureAny()) to make the distinction clearer?
  3. Use Cases: Are there other exception handling patterns we should consider supporting?

Thoughts?

Summary by CodeRabbit

  • New Features

    • Improved conditional composition to route success/failure flows based on observable runtime state.
    • Added type-specific recover and onFailure overloads (including varargs support and null-safe filtering) for finer-grained error handling.
  • Tests

    • Expanded unit tests covering type-filtered recover/onFailure (single and varargs), null/edge cases, chaining, and real-world logging scenarios.
  • Documentation

    • Enhanced inline docs and examples for the new overloads.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Nov 7, 2025

Walkthrough

Added exception-type-specific overloads for recover and onFailure (single-type and varargs), adjusted compose to use observable state in its conditional path, and expanded unit tests to cover the new behaviors and edge cases.

Changes

Cohort / File(s) Change Summary
Try API Enhancements
src/main/java/org/zeplinko/commons/lang/ext/core/Try.java
Added type-filtered recover overloads (recover(Class<? extends E>, Function...), @SafeVarargs recover(Function..., Class<? extends E>...)) and type-filtered onFailure overloads (onFailure(Class<? extends E>, Consumer...), @SafeVarargs onFailure(Consumer..., Class<? extends E>...)). Updated compose conditional path to consult observable state (isSuccess()/getData()/getError()). Introduced Arrays usage for varargs null-filtering and expanded inline docs/examples.
Try Test Expansion
src/test/java/org/zeplinko/commons/lang/ext/core/TryTest.java
Added comprehensive tests for the new recover and onFailure overloads (single-type and varargs), including null/empty varargs handling, matching/non-matching exception flows, chaining across multiple handlers, and logging/real-world scenarios.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant TryObj as Try<T>
  participant Cond as Predicate<Try<T>>
  participant Success as successMapper
  participant Failure as failureMapper
  participant Result as Try<U>

  Caller->>TryObj: compose(condition, successMapper, failureMapper)
  TryObj->>Cond: evaluate condition(TryObj)
  alt condition true
    TryObj->>Success: apply successMapper (uses isSuccess()/getData())
    Success-->>Result: mapped Try<U>
  else condition false
    TryObj->>Failure: apply failureMapper (uses isSuccess()/getError())
    Failure-->>Result: mapped Try<U>
  end
  Result-->>Caller: return Try<U>
Loading
sequenceDiagram
  participant Caller
  participant TryObj as Try<T>
  participant Err as Exception
  participant Matcher as type-matcher (Arrays filter)
  participant RecoverMap as failureMapper
  participant Result as Try<T>

  Caller->>TryObj: recover(exceptionType(s), failureMapper)
  TryObj->>Err: obtain current error (if any)
  TryObj->>Matcher: check Err against provided exceptionType(s)
  alt match found
    TryObj->>RecoverMap: invoke failureMapper(Exception) -> Try<T>
    RecoverMap-->>Result: mapped Try<T>
  else no match or no error
    TryObj-->>Result: unchanged Try<T>
  end
  Result-->>Caller: return Try<T>
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Review generic bounds and variance on the new recover/onFailure signatures.
  • Verify null checks and NPE-safe behavior for varargs (null elements and null array).
  • Confirm correct use of @SafeVarargs and that methods are final where required.
  • Validate compose delegation and that state checks (isSuccess/getData/getError) match intent.

Possibly related issues

Poem

🐇 I hopped through Try and sniffed each case,
Matched errors by type with delicate grace,
When matches came, I mended the fall,
Else I left it be — I heard the call,
Nibbles and fixes for every trace 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.24% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly describes the main change: adding the ability to recover from specific exceptions with type-filtering overloads.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 51b5acd and d6dcd16.

📒 Files selected for processing (2)
  • src/main/java/org/zeplinko/commons/lang/ext/core/Try.java (5 hunks)
  • src/test/java/org/zeplinko/commons/lang/ext/core/TryTest.java (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/test/java/org/zeplinko/commons/lang/ext/core/TryTest.java (1)
src/main/java/org/zeplinko/commons/lang/ext/core/Try.java (1)
  • Try (73-580)
🔇 Additional comments (10)
src/main/java/org/zeplinko/commons/lang/ext/core/Try.java (6)

5-5: LGTM!

The Arrays import is correctly used for null-filtering in the varargs-based recover and onFailure methods.


192-192: LGTM!

The refactoring from isFailure() to isSuccess() maintains logical equivalence while arguably improving readability by leading with the success path.


222-266: Excellent implementation of type-specific recovery!

The method correctly:

  • Validates parameters to prevent NPE
  • Returns this unchanged for success or non-matching exceptions
  • Applies the recovery function only when the exception type matches
  • Handles subclass matching via isInstance()

The comprehensive Javadoc with concrete examples is particularly helpful.


268-344: Well-designed varargs recovery method!

The implementation correctly:

  • Validates both the failure mapper and the exception types array
  • Filters out null elements in the varargs array with Objects::nonNull
  • Returns this unchanged if no exception types match
  • Applies recovery when any exception type matches

The Arrays.stream() approach is readable and appropriate for exception-handling paths. The comprehensive documentation with multiple examples demonstrates thoughtful API design.


408-465: LGTM! Clean implementation of type-specific error handling.

The method correctly:

  • Validates parameters
  • Executes the consumer only when this is a failure AND the exception type matches
  • Returns this for fluent chaining
  • Handles subclass matching appropriately

The practical examples demonstrating logging and metrics use cases are excellent documentation.


467-543: Excellent varargs error-handling implementation!

The method correctly:

  • Validates both the consumer and the exception types array
  • Filters out null elements in the varargs array
  • Executes the consumer only when this is a failure AND any exception type matches
  • Returns this for fluent chaining

The implementation is consistent with the varargs recover method's approach. The real-world examples effectively demonstrate combining multiple handlers for different exception groups.

src/test/java/org/zeplinko/commons/lang/ext/core/TryTest.java (4)

350-409: Comprehensive test coverage for single-exception recover!

The tests thoroughly validate:

  • Null parameter handling
  • Success path preservation
  • Non-matching exception types
  • Exact and subclass exception matching
  • Recovery functions that return either success or failure

The test names clearly describe the scenarios being tested.


512-613: Excellent test coverage for single-exception onFailure!

The tests comprehensively validate:

  • Null parameter handling for both exception type and consumer
  • Correct conditional execution based on exception type matching
  • Subclass matching behavior
  • Chaining scenarios where only matching handlers execute

The chaining tests (lines 582-613) effectively demonstrate practical usage patterns with multiple exception types.


617-848: Outstanding comprehensive test coverage for varargs onFailure!

The test suite thoroughly validates:

  • All parameter validation scenarios
  • Success and failure paths with various matching conditions
  • Exception matching at different positions in the varargs array
  • Edge cases: empty arrays, null elements, all-null arrays
  • Subclass matching behavior
  • Chaining patterns with different exception groups
  • Real-world logging scenarios

The coverage of null-element filtering (lines 761-791) demonstrates careful attention to defensive programming. The real-world scenarios (lines 794-848) effectively showcase practical API usage.


869-1089: Excellent comprehensive test coverage for varargs recover!

The test suite thoroughly validates:

  • All parameter validation scenarios
  • Success and failure paths with various matching conditions
  • Exception matching at different positions in the varargs array
  • Edge cases: empty arrays, null elements, all-null arrays
  • Subclass matching behavior
  • Recovery functions that return either success or failure
  • Chaining patterns with different exception groups

The test structure mirrors the varargs onFailure tests, providing consistent coverage across similar APIs. The real-world scenarios effectively demonstrate practical usage patterns.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Nov 7, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0be5cf6 and 699cc42.

📒 Files selected for processing (2)
  • src/main/java/org/zeplinko/commons/lang/ext/core/Try.java (5 hunks)
  • src/test/java/org/zeplinko/commons/lang/ext/core/TryTest.java (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/test/java/org/zeplinko/commons/lang/ext/core/TryTest.java (2)
src/main/java/org/zeplinko/commons/lang/ext/core/Try.java (1)
  • Try (74-645)
src/main/java/org/zeplinko/commons/lang/ext/core/AbstractOutcome.java (1)
  • SuppressWarnings (44-186)
🔇 Additional comments (12)
src/test/java/org/zeplinko/commons/lang/ext/core/TryTest.java (5)

320-407: LGTM! Comprehensive test coverage for conditional compose.

The tests thoroughly validate the new compose(Predicate, Function, Function) method with excellent coverage of null parameter handling, both condition outcomes, custom business logic conditions, and failure propagation scenarios.


438-497: LGTM! Thorough coverage of type-specific recovery.

The tests effectively validate the single-exception-type recover method, including proper null handling, exception type matching (exact and subclass), and recovery function application only when conditions are met.


600-701: LGTM! Excellent test coverage for type-specific failure handling.

The tests comprehensively validate the single-exception-type onFailure method, including proper chaining behavior and real-world scenarios like combining specific handlers with catch-all error handling.


703-936: LGTM! Outstanding test coverage for varargs onFailure.

The tests provide comprehensive validation of the varargs onFailure method, with excellent coverage of edge cases (empty arrays, null elements), multiple exception type scenarios, and real-world patterns like logging and chaining different exception groups.


957-1177: LGTM! Comprehensive test coverage for varargs recover.

The tests thoroughly validate the varargs recover method with excellent coverage of edge cases, multiple exception type scenarios, and chaining patterns. The test structure mirrors the varargs onFailure tests, maintaining consistency across the test suite.

src/main/java/org/zeplinko/commons/lang/ext/core/Try.java (7)

5-5: LGTM! Necessary imports for new functionality.

The Arrays and Predicate imports are required for the varargs exception handling and conditional composition features respectively.

Also applies to: 11-11


187-192: LGTM! Clean refactoring to eliminate duplication.

The delegation to the conditional compose method maintains backward compatibility while eliminating code duplication. Using Try::isSuccess as the condition preserves the original behavior.


194-257: LGTM! Well-designed conditional composition with excellent documentation.

The new conditional compose method provides powerful flexibility for custom branching logic. The javadoc is clear with practical examples, and the implementation correctly applies the appropriate mapper based on the condition evaluation.


286-330: LGTM! Type-specific recovery with correct matching logic.

The single-exception-type recover method correctly implements exception type matching using isInstance, properly handles success cases and non-matching exceptions, and delegates cleanly to the conditional compose method.


332-409: LGTM! Varargs recover enables clean multi-exception handling.

Aside from the stray semicolon on line 401, the implementation correctly handles multiple exception types with a single recovery function. The null element filtering and exception type matching logic work as documented.


473-530: LGTM! Type-specific failure handling with clean implementation.

The single-exception-type onFailure method provides a clean way to handle specific exceptions differently (e.g., logging at different levels). The implementation correctly executes the consumer only when the Try is a failure and the exception type matches, and properly returns this for chaining.


532-608: LGTM! Varargs onFailure completes the exception handling toolkit.

The varargs onFailure method provides a convenient way to handle multiple exception types with a single consumer. The implementation correctly filters null elements, matches exception types using anyMatch, and maintains proper chaining behavior.

Comment thread src/main/java/org/zeplinko/commons/lang/ext/core/Try.java Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
src/main/java/org/zeplinko/commons/lang/ext/core/Try.java (3)

319-330: Consider clarifying the unused lambda parameter.

On line 327, the lambda currentTry -> this receives the success data but ignores it in favor of returning the original Try instance. While functionally correct, the parameter name currentTry is misleading since it receives T (the data), not Try<T>.

Consider renaming for clarity:

         return compose(
                 aTry -> aTry.isSuccess() || !exceptionType.isInstance(aTry.getError()),
-                currentTry -> this,
+                data -> this,
                 failureMapper
         );

This makes it clearer that the data parameter is intentionally unused.


395-408: Consider clarifying the unused lambda parameter.

Similar to the single-type recover, line 405 has a lambda currentTry -> this where the parameter name is misleading.

         return compose(
                 aTry -> aTry.isSuccess() || Arrays.stream(exceptionTypes)
                         .filter(Objects::nonNull)
                         .noneMatch(exceptionType -> exceptionType.isInstance(this.getError())),
-                currentTry -> this,
+                data -> this,
                 failureMapper
         );

319-321: Note: Parameter ordering inconsistency between single-type and varargs methods.

The parameter ordering differs between single-type and varargs variants:

  • Single-type: recover(Class, Function) and onFailure(Class, Consumer)
  • Varargs: recover(Function, Class...) and onFailure(Consumer, Class...)

While this is intentional to avoid Java overload ambiguity (as noted in the PR description), it may slightly impact API intuitiveness. Consider whether method name differentiation (e.g., recoverAny, onFailureAny) might provide better ergonomics by allowing consistent parameter ordering across all variants.

This is noted as a design consideration rather than a required change, given the thoughtful rationale in the PR description.

Also applies to: 395-397, 519-521, 593-595

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 699cc42 and 51b5acd.

📒 Files selected for processing (1)
  • src/main/java/org/zeplinko/commons/lang/ext/core/Try.java (5 hunks)
🔇 Additional comments (5)
src/main/java/org/zeplinko/commons/lang/ext/core/Try.java (5)

5-5: LGTM!

The new imports are appropriately added for the enhanced functionality.

Also applies to: 11-11


187-192: LGTM!

The delegation to the new conditional compose method maintains backward compatibility while reducing code duplication.


519-529: LGTM!

The implementation correctly checks the exception type and executes the consumer only when appropriate, maintaining proper null safety and method chaining.


593-607: LGTM!

The varargs implementation correctly handles multiple exception types with proper null filtering and appropriate use of anyMatch for the matching logic.


248-257: The compose method is functioning as designed; the review comment mischaracterizes architectural flexibility as a bug.

The compose method with custom Predicate<Try<T>> is intentionally designed to allow sophisticated, caller-controlled conditions beyond simple success checks. This is not a defect:

  1. Safe default API exists: The simple compose(Function, Function) overload (lines 187–192) uses Try::isSuccess as the condition, ensuring alignment and eliminating null risks for standard use cases.

  2. Contract is documented: The javadoc (lines 194–247) explicitly states, "The condition has access to the entire Try instance, not just the success / failure state. This enables sophisticated branching logic beyond simple success checks." It demonstrates proper usage with conditions like aTry -> aTry.isSuccess() && aTry.getData() > 0.

  3. Internal consistency: All internal uses within the class—recover methods at lines 325–329 and 401–407—pair conditions with aligned mappers (e.g., conditions checking isSuccess() before invoking success logic).

  4. Intentional design: getData() and getError() return null when not applicable (success stores data with error=null, failure stores error with data=null). This is by design, not oversight.

Callers invoking the condition-based overload are expected to ensure condition and mapper alignment, as documented. Violating this contract is misuse, not a platform defect.

Likely an incorrect or invalid review comment.

@a-anand-91119
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Nov 8, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Comment thread src/main/java/org/zeplinko/commons/lang/ext/core/Try.java
@ShivamNagpal ShivamNagpal merged commit c6f90f4 into Zeplinko:develop Nov 12, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants