failsafe-lib / failsafe

Fault tolerance and resilience patterns for the JVM
https://failsafe.dev
Apache License 2.0
4.2k stars 297 forks source link

When using Fallback.ofException(...) the desired Exception gets wrapped by FailsafeException #270

Closed akopper closed 3 years ago

akopper commented 3 years ago

Exceptions not inheriting from RuntimeException or Error are wrapped in FailsafeException

Consider the following

@Test
public void testFallbackException() {
    Fallback<Object> fallback = Fallback.ofException(e -> new Exception("Custom Exception", e.getLastFailure()));
    Failsafe.with(fallback)
        .get(() ->
             {
                throw new Exception("Exception from execution");
             });
}

The result will be:

net.jodah.failsafe.FailsafeException: java.lang.Exception: Custom Exception

    at net.jodah.failsafe.FailsafeExecutor.call(FailsafeExecutor.java:383)
    at net.jodah.failsafe.FailsafeExecutor.get(FailsafeExecutor.java:67)
    at com.iLogs.iPCP.parking.utils.FailsafeTest.testFallbackException(FailsafeTest.java:35)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
    at org.junit.rules.ExternalResource$1.evaluate(ExternalResource.java:48)
    at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
    at org.junit.rules.RunRules.evaluate(RunRules.java:20)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at org.junit.rules.ExternalResource$1.evaluate(ExternalResource.java:48)
    at org.junit.rules.RunRules.evaluate(RunRules.java:20)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
Caused by: java.lang.Exception: Custom Exception
    at com.iLogs.iPCP.parking.utils.FailsafeTest.lambda$testFallbackException$0(FailsafeTest.java:33)
    at net.jodah.failsafe.Fallback.lambda$ofException$0(Fallback.java:119)
    at net.jodah.failsafe.Fallback.apply(Fallback.java:235)
    at net.jodah.failsafe.FallbackExecutor.lambda$supply$0(FallbackExecutor.java:48)
    at net.jodah.failsafe.Execution.executeSync(Execution.java:129)
    at net.jodah.failsafe.FailsafeExecutor.call(FailsafeExecutor.java:376)
    ... 32 more
Caused by: java.lang.Exception: Exception from execution
    at com.iLogs.iPCP.parking.utils.FailsafeTest.lambda$testFallbackException$1(FailsafeTest.java:37)
    at net.jodah.failsafe.Functions.lambda$get$0(Functions.java:48)
    at net.jodah.failsafe.FallbackExecutor.lambda$supply$0(FallbackExecutor.java:43)
    ... 34 more

I would expect java.lang.Exception: Custom Exception to be the outermost Exception.

This behaviour can be traced back to https://github.com/jhalterman/failsafe/blob/f9ddba08a7b218c0f14dc8f713709b2e28aeab65/src/main/java/net/jodah/failsafe/FailsafeExecutor.java#L379-L383

Is there a reason for this?

jhalterman commented 3 years ago

Yes, this is mentioned in the Javadocs, ex: https://failsafe-lib.github.io/javadoc/net/jodah/failsafe/FailsafeExecutor.html#get-net.jodah.failsafe.function.CheckedSupplier-

As for why things work this way, it was a design decision to wrap checked exceptions from user-supplied synchronously executed code. The alternative would have been for FailsafeExecutor.get to declare throws Exception, which would require all calling code to handle checked exceptions, whether they're likely to be thrown or not. Yet another alternative would have been to use the sneaky throws hack to throw checked exceptions without declaring them, but that may be somewhat surprising for users.