UncheckedIOException and suppressed exception

Zhong Yu zhong.j.yu at gmail.com
Tue Feb 5 09:04:10 PST 2013


On Tue, Feb 5, 2013 at 3:47 AM, Peter Levart <peter.levart at gmail.com> wrote:
>
> On 02/05/2013 04:47 AM, David Holmes wrote:
>
> On 5/02/2013 1:39 PM, Zhong Yu wrote:
>
> On Mon, Feb 4, 2013 at 7:56 PM, David Holmes<david.holmes at oracle.com>
> wrote:
>
> On 5/02/2013 10:16 AM, Zhong Yu wrote:
>
> Suppose we have an UncheckedIOException e1, which wraps an IOException
> e2. Then another exception e3 occurs that we want suppress. Should we
> do e1.addSuppressed(e3), or e2.addSuppressed(e3)? That's pretty
> confusing.
>
> I don't see any confusion except for your numbering. You have one exception
> that is inflight, e1, and then e3 occurs. If you want to continue throwing
> e1 then e1 is suppressing e3.
>
> The catcher of an UncheckedIOException e1 would see it simply as a
> container, so it'll extract the contained IOException e2, and deal
> with e2 afterwards, for example, rethrow e2. If e3 was added as e1's
> suppressed exception, e3 is then lost.
>
> Then the catch is showing their ignorance of suppressed exceptions. BUt
> this is not specific to this situation. Anytime you catch an exception
> with a cause and a suppression list you have to decide what it is you
> want to do with it.
>
>
> I agree that suppressed exceptions should be (and are) attached to
> "top-level" thrown exceptions, whatever they are. Also in the situation of
> UncheckedIOException wrapper for IOException. There would be more confusion
> otherwise. But here the "duality" strikes again. I just want to discuss
> possible techiques how to deal with that...
>
> When the body of the try-with-resources construct throws both types
> (IOException and UncheckedIOException), handlers that want to analyze
> suppressed exceptions have to extract them from both types separately. You
> might think multi-catch is the answer:
>
>         try (...) {
>             ...
>         }
>         catch (IOException | UncheckedIOException e) {
>             Throwable[] suppressed = e.getSuppressed();
>             ...
>         }
>
> ...but things get complicated when the same handler wants to deal with the
> "potential" cause first (in case of UncheckedIOException):
>
>         try (...) {
>             ...
>         }
>         catch (IOException | UncheckedIOException e) {
>             IOException ioe = e instanceof IOException ? (IOException) e :
> ((UncheckedIOException) e).getCause();
>             ...
>             Throwable[] suppressed = e.getSuppressed();
>             ...
>         }
>
> ...so maybe the following pattern is more appropriate:
>
>         try (...) {
>             try {
>                 ...
>             }
>             catch (UncheckedIOException uioe) { throw uioe.getCause(); }
>         }
>         catch (IOException ioe) {
>             ...
>             Throwable[] suppressed = e.getSuppressed();
>             ...
>         }
>
> Now in another thread (CloseableStream exceptions), Alan argued that getting
> Path objects is only half the story. A program would usually want to do
> something with them (do some IO on them perhaps) and those operations
> traditionally throw IOExceptions of multiple subtypes. The programmer has to
> decide how these operations are to be performed. There are basically two
> options:
>
> stay inside of the streams API and perform them inside lambdas. In this case
> the operations themselves would have to wrap IOExceptions with
> UncheckedIOExceptions. Therefore we could get multiple subtypes of
> IOException wrapped in UncheckedIOException - a situation for the handler to
> deal with.
> escape streams API by collecting final Paths into a collection and doing
> external iteration on the result, performing IO operations "in the open". In
> this case IOExceptions would be thrown directly, but we still would have to
> take into account the UncheckedIOExceptions originating from the
> Stream<Path> producing operations.
>
>
> The style that is to be chosen depends largely on what the program wants to
> achieve. One might argue that the programmer will choose the streams API for
> it's main feature - to employ parallelism. In this case he/she would want to
> stay inside the streams API and would have to deal with handling the
> multiple subtypes of IOException wrapped with UncheckedIOException. The
> following pattern seems to be the most general for that case and others:
>
>         try (CloseableStream<Path> = ...) {
>             try {
>                 ... direct Path operations throwing IOException and/or
>                 ... Stream<Path> operations throwing UncheckedIOException
>             }
>             catch (UncheckedIOException uioe) { throw uioe.getCause(); }
>         }
>         catch (EOFException eofe) {
>             // ...
>         }
>         catch (ZipException ze) {
>             // ...
>         }
>         catch (IOException ioe) {
>             //...
>         }

Can you give a more concrete, real-world example?

P.S. your pattern will also lose any information attached to
UncheckedIOException, including message, stacktrace, suppressed
exceptions.

> That's not so bad. So at the end I feel that the decision about
> CloseableStream factory methods throwing IOException is the right one when
> considering broader view on the matter. The above pattern would not be
> possible otherwise.
>
> Regards, Peter
>
>
> This "containing" relation is different from the usual "causal"
> relation between an exception and its cause. Let's be honest, the only
> purpose of an UncheckedIOException is to smuggle an IOException as
> unchecked; UncheckedIOException itself has no "business meaning".
>
> Again read the Throwable javadoc that Joe pointed out top you. This
> "conversion" situation is one of the primary uses for setting a cause:
>
> "A second reason that a throwable may have a cause is that the method
> that throws it must conform to a general-purpose interface that does not
> permit the method to throw the cause directly. For example, suppose a
> persistent collection conforms to the Collection interface, and that its
> persistence is implemented atop java.io. Suppose the internals of the
> add method can throw an IOException. The implementation can communicate
> the details of the IOException to its caller while conforming to the
> Collection interface by wrapping the IOException in an appropriate
> unchecked exception. (The specification for the persistent collection
> should indicate that it is capable of throwing such exceptions.) "
>
> David
> -----
>
> If UncheckedIOException is not a *real* exception, maybe we should
> make it a "control" exception. It should have no stacktrace; it should
> have no cause (the wrapped exception is not a cause); it should not
> contain suppressed exceptions.
>
> Any wrapping exception should have the original exception as its cause. Why
> should this wrapper be any different? Throwable even defines it so:
>
> " Throwing a "wrapped exception" (i.e., an exception containing a cause)
> ..."
>
> Suppression is about control over which exception is propagating - so again
> why should this be a special case?
>
>
> Maybe it can override addSuppressed() to forward suppressed exceptions
> to the wrapped exception. (Though addSuppressed() is `final`, there's
> no problem to leave a backdoor for another JDK class)
>
> I think your exception processing model is slightly confused here.
>
> David
> -----
>
> Zhong Yu
>
>


More information about the lambda-dev mailing list