UncheckedIOException and suppressed exception

Peter Levart peter.levart at gmail.com
Tue Feb 5 01:47:39 PST 2013

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:

 1. 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.
 2. 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) {

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