Developing in Java an asynchronous method with a CompletableFuture
return type we expect the resulting CF to complete normally or exceptionally depending on whether that method succeeds or fails.
Yet, consider for instance that my method writes to an AsynchronousChannel
and got an exception opening that channel. It has not even started writing. So, in this case I am tenting to just let the exception flow to the caller. Is that correct?
Although the caller will have to deal with 2 failure scenarios: 1) exception, or 2) rejected promise.
Or alternatively, should my method catch that exception and return a rejected promise instead?
As we know, in asynchronous programming, control does not wait for the function's result and it executes the next line. So when the function throws an exception, at that moment the program control is out of the try-catch block.
Most exceptions occur synchronously as a result of an action by the thread in which they occur, and at a point in the program that is specified to possibly result in such an exception. An asynchronous exception is, by contrast, an exception that can potentially occur at any point in the execution of a program.
Exceptions should be thrown when the contract between a method and its caller cannot be fulfilled.
IMO, option 1) makes the API harder to use because there will be two different paths for communicating errors:
The programmer now has to ensure both these two paths are correctly handled, instead of just one.
It is also interesting to observe that the behaviour for both C# and Javascript is to always report exceptions thrown inside the body of an async
function via the returned Task
/Promise
, even for exceptions thrown before the first await
, and never by ending the async
function call with an exception.
The same is also true for Kotlin's coroutines, even when using the Unconfined
dispatcher
class SynchronousExceptionExamples {
@Test
fun example() {
log.info("before launch")
val job = GlobalScope.launch(Dispatchers.Unconfined) {
log.info("before throw")
throw Exception("an-error")
}
log.info("after launch")
Thread.sleep(1000)
assertTrue(job.isCancelled)
}
}
will produce
6 [main] INFO SynchronousExceptionExamples - before launch
73 [main @coroutine#1] INFO SynchronousExceptionExamples - before throw
(...)
90 [main] INFO SynchronousExceptionExamples - after launch
Note as the exception occurs in the main
thread, however launch
ends with a proper Job
.
I think both are valid designs. Datastax actually started their design with first approach, where borrowing a connection was blocking, and switched to fully async model (https://docs.datastax.com/en/developer/java-driver/3.5/upgrade_guide/#3-0-4)
As a user of datastax java driver I was very happy with the fix, as it changed the api to be truly non-blocking (even opening a channel, in your example, has a cost).
But I don't think there are right and wrong here...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With