Exceptions in an async code

classic Classic list List threaded Threaded
4 messages Options
Reply | Threaded
Open this post in threaded view
|

Exceptions in an async code

Pavel Rappo
Hello,

Does anybody have any experience with designing exception contracts for
signatures like this?

    CompletableFuture<E> someMethod(T1 argOfTypeT1, ... Tn
argOfTypeTn) throws X;

As I understand, there is a generally accepted idea on throwing exceptions from
methods, which basically says that an exception should generally be thrown as
early as possible. It seems to work fine with an "ordinary code", or synchronous
for that matter.

With a CompletableFuture we have a bit of a different situation. Not only it
provides its own channel for exceptions, which are not thrown to, but passed to
as arguments, it also allows an ultimate deferral. That is, if not checked for,
an exception might stay unobserved forever.

I heard an opinion that once one goes with methods like the above, one should
better go the full way and relay _all_ exceptions through the returned CF. The
rationale behind this is that this way one will only have a single place to deal
with a failure thus would be able handle it in a more robust way.

This sounds very rational. But as usual, the devil is in the detail. What about
all these NullPointerException, IllegalArgumentException, etc.?

For instance, java.util.concurrent.Flow.Subscription.request _relay_ the
exception to another method, rather than throws it in-place:

    /**
     * Adds the given number {@code n} of items to the current
     * unfulfilled demand for this subscription.  If {@code n} is
     * negative, the Subscriber will receive an {@code onError}
     * signal with an {@link IllegalArgumentException} argument.
    ...
    public void request(long n);

On the other hand,
java.util.concurrent.ExecutorService.submit(java.util.concurrent.Callable<T>)
throws exceptions (related to the executor itself) in-place.

    ...
     * @throws RejectedExecutionException if the task cannot be
     *         scheduled for execution
     * @throws NullPointerException if the task is null
     */
    <T> Future<T> submit(Callable<T> task);

Is there any comparative analysis available on the web, or known pitfalls of
going one way or another?

Thanks,
-Pavel
_______________________________________________
Concurrency-interest mailing list
[hidden email]
http://cs.oswego.edu/mailman/listinfo/concurrency-interest
Reply | Threaded
Open this post in threaded view
|

Re: Exceptions in an async code

Vitaly Davidovich
I've seen both approaches (all exceptions signaled via futures vs hybrid).  I'm on the fence regarding runtime exceptions such as NPE, IAE, etc that flat out violate an API's contract (passing a null arg, passing an out of range parameter, etc).  Signaling those exceptions via a future may result in significant "distance" between where the exception occurred and where it's checked, and of course can possibly go unobserved entirely, as you said.  In some ways, throwing those types of exceptions inline is better.

What I *do* find very annoying is when the same error condition is signaled sometimes synchronously and other times asynchronously.  For instance, initiating an I/O operation against a socket.  I've seen APIs that will throw an inline exception if the socket is already closed, but if the socket is closed while the I/O is still pending, it'll signal that via the future.  You end up with the same handler logic for both cases, which is very annoying.  So for these types of cases, I prefer signaling the error via the future.  I guess the generalization is "if the error condition can occur while the async operation is in flight but also immediately, always signal via the future".

So for me, non-runtime exceptions should always be signaled via the future, even if the future completes with the error immediately.  Runtime exceptions I can see both ways, but would likely prefer immediate feedback via an inline exception. 



On Wed, May 11, 2016 at 6:42 AM, Pavel Rappo <[hidden email]> wrote:
Hello,

Does anybody have any experience with designing exception contracts for
signatures like this?

    CompletableFuture<E> someMethod(T1 argOfTypeT1, ... Tn
argOfTypeTn) throws X;

As I understand, there is a generally accepted idea on throwing exceptions from
methods, which basically says that an exception should generally be thrown as
early as possible. It seems to work fine with an "ordinary code", or synchronous
for that matter.

With a CompletableFuture we have a bit of a different situation. Not only it
provides its own channel for exceptions, which are not thrown to, but passed to
as arguments, it also allows an ultimate deferral. That is, if not checked for,
an exception might stay unobserved forever.

I heard an opinion that once one goes with methods like the above, one should
better go the full way and relay _all_ exceptions through the returned CF. The
rationale behind this is that this way one will only have a single place to deal
with a failure thus would be able handle it in a more robust way.

This sounds very rational. But as usual, the devil is in the detail. What about
all these NullPointerException, IllegalArgumentException, etc.?

For instance, java.util.concurrent.Flow.Subscription.request _relay_ the
exception to another method, rather than throws it in-place:

    /**
     * Adds the given number {@code n} of items to the current
     * unfulfilled demand for this subscription.  If {@code n} is
     * negative, the Subscriber will receive an {@code onError}
     * signal with an {@link IllegalArgumentException} argument.
    ...
    public void request(long n);

On the other hand,
java.util.concurrent.ExecutorService.submit(java.util.concurrent.Callable<T>)
throws exceptions (related to the executor itself) in-place.

    ...
     * @throws RejectedExecutionException if the task cannot be
     *         scheduled for execution
     * @throws NullPointerException if the task is null
     */
    <T> Future<T> submit(Callable<T> task);

Is there any comparative analysis available on the web, or known pitfalls of
going one way or another?

Thanks,
-Pavel
_______________________________________________
Concurrency-interest mailing list
[hidden email]
http://cs.oswego.edu/mailman/listinfo/concurrency-interest


_______________________________________________
Concurrency-interest mailing list
[hidden email]
http://cs.oswego.edu/mailman/listinfo/concurrency-interest
Reply | Threaded
Open this post in threaded view
|

Re: Exceptions in an async code

Alex Otenko
In reply to this post by Pavel Rappo
Once you are into Futures, you are essentially into continuation-passing. Your code is cleaner if you always consider E someMethod(T1 argOfTypeT1, … Tn argOfTypeTn) throws X; to be a continuation-passing transform of CompletableFuture<Either<X,E>> someMethod(T1 argOfTypeT1, … Tn argOfTypeTn); (and vice versa). If you code like that, you never “forget” to check for exceptions - they are just part of what the CompletableFuture type means. But, of course, it isn’t coded like that.

Making sure all runtime exceptions propagate to the consumer of the Future is more consistent. If they are thrown to the caller, it is potentially leaking abstraction, but in the wrong direction, which is worse than leaking abstractions in the right direction.

Alex

> On 11 May 2016, at 11:42, Pavel Rappo <[hidden email]> wrote:
>
> Hello,
>
> Does anybody have any experience with designing exception contracts for
> signatures like this?
>
>    CompletableFuture<E> someMethod(T1 argOfTypeT1, ... Tn
> argOfTypeTn) throws X;
>
> As I understand, there is a generally accepted idea on throwing exceptions from
> methods, which basically says that an exception should generally be thrown as
> early as possible. It seems to work fine with an "ordinary code", or synchronous
> for that matter.
>
> With a CompletableFuture we have a bit of a different situation. Not only it
> provides its own channel for exceptions, which are not thrown to, but passed to
> as arguments, it also allows an ultimate deferral. That is, if not checked for,
> an exception might stay unobserved forever.
>
> I heard an opinion that once one goes with methods like the above, one should
> better go the full way and relay _all_ exceptions through the returned CF. The
> rationale behind this is that this way one will only have a single place to deal
> with a failure thus would be able handle it in a more robust way.
>
> This sounds very rational. But as usual, the devil is in the detail. What about
> all these NullPointerException, IllegalArgumentException, etc.?
>
> For instance, java.util.concurrent.Flow.Subscription.request _relay_ the
> exception to another method, rather than throws it in-place:
>
>    /**
>     * Adds the given number {@code n} of items to the current
>     * unfulfilled demand for this subscription.  If {@code n} is
>     * negative, the Subscriber will receive an {@code onError}
>     * signal with an {@link IllegalArgumentException} argument.
>    ...
>    public void request(long n);
>
> On the other hand,
> java.util.concurrent.ExecutorService.submit(java.util.concurrent.Callable<T>)
> throws exceptions (related to the executor itself) in-place.
>
>    ...
>     * @throws RejectedExecutionException if the task cannot be
>     *         scheduled for execution
>     * @throws NullPointerException if the task is null
>     */
>    <T> Future<T> submit(Callable<T> task);
>
> Is there any comparative analysis available on the web, or known pitfalls of
> going one way or another?
>
> Thanks,
> -Pavel
> _______________________________________________
> Concurrency-interest mailing list
> [hidden email]
> http://cs.oswego.edu/mailman/listinfo/concurrency-interest


_______________________________________________
Concurrency-interest mailing list
[hidden email]
http://cs.oswego.edu/mailman/listinfo/concurrency-interest
Reply | Threaded
Open this post in threaded view
|

Re: Exceptions in an async code

Viktor Klang
Also, if you want to shield the calling code for spurious exceptions, you can always invoke the method within a future and then do a thenComposeAsync(identity)

On Wed, May 11, 2016 at 9:14 AM, Alex Otenko <[hidden email]> wrote:
Once you are into Futures, you are essentially into continuation-passing. Your code is cleaner if you always consider E someMethod(T1 argOfTypeT1, … Tn argOfTypeTn) throws X; to be a continuation-passing transform of CompletableFuture<Either<X,E>> someMethod(T1 argOfTypeT1, … Tn argOfTypeTn); (and vice versa). If you code like that, you never “forget” to check for exceptions - they are just part of what the CompletableFuture type means. But, of course, it isn’t coded like that.

Making sure all runtime exceptions propagate to the consumer of the Future is more consistent. If they are thrown to the caller, it is potentially leaking abstraction, but in the wrong direction, which is worse than leaking abstractions in the right direction.

Alex

> On 11 May 2016, at 11:42, Pavel Rappo <[hidden email]> wrote:
>
> Hello,
>
> Does anybody have any experience with designing exception contracts for
> signatures like this?
>
>    CompletableFuture<E> someMethod(T1 argOfTypeT1, ... Tn
> argOfTypeTn) throws X;
>
> As I understand, there is a generally accepted idea on throwing exceptions from
> methods, which basically says that an exception should generally be thrown as
> early as possible. It seems to work fine with an "ordinary code", or synchronous
> for that matter.
>
> With a CompletableFuture we have a bit of a different situation. Not only it
> provides its own channel for exceptions, which are not thrown to, but passed to
> as arguments, it also allows an ultimate deferral. That is, if not checked for,
> an exception might stay unobserved forever.
>
> I heard an opinion that once one goes with methods like the above, one should
> better go the full way and relay _all_ exceptions through the returned CF. The
> rationale behind this is that this way one will only have a single place to deal
> with a failure thus would be able handle it in a more robust way.
>
> This sounds very rational. But as usual, the devil is in the detail. What about
> all these NullPointerException, IllegalArgumentException, etc.?
>
> For instance, java.util.concurrent.Flow.Subscription.request _relay_ the
> exception to another method, rather than throws it in-place:
>
>    /**
>     * Adds the given number {@code n} of items to the current
>     * unfulfilled demand for this subscription.  If {@code n} is
>     * negative, the Subscriber will receive an {@code onError}
>     * signal with an {@link IllegalArgumentException} argument.
>    ...
>    public void request(long n);
>
> On the other hand,
> java.util.concurrent.ExecutorService.submit(java.util.concurrent.Callable<T>)
> throws exceptions (related to the executor itself) in-place.
>
>    ...
>     * @throws RejectedExecutionException if the task cannot be
>     *         scheduled for execution
>     * @throws NullPointerException if the task is null
>     */
>    <T> Future<T> submit(Callable<T> task);
>
> Is there any comparative analysis available on the web, or known pitfalls of
> going one way or another?
>
> Thanks,
> -Pavel
> _______________________________________________
> Concurrency-interest mailing list
> [hidden email]
> http://cs.oswego.edu/mailman/listinfo/concurrency-interest


_______________________________________________
Concurrency-interest mailing list
[hidden email]
http://cs.oswego.edu/mailman/listinfo/concurrency-interest



--
Cheers,

_______________________________________________
Concurrency-interest mailing list
[hidden email]
http://cs.oswego.edu/mailman/listinfo/concurrency-interest