Suppressed Exceptions

A suppressed exception is an exception that is ignored to let another exception be thrown from the method.

Java provides a robust and efficient exception handling framework used in all sorts of applications irrespective of their size. Moreover, it allows us to create custom exceptions and handlers to better drive the application flow.

If something goes wrong during the execution of a method, the method creates an instance of an Exception class that contains all the relevant details in the form of the stack trace and hands it over to the runtime, i.e., JVM. This process is known as throwing an exception.

The stack trace contains an ordered list of the method call hierarchy that was followed originally to reach the point where that exception has occurred.

Using the details, the JVM will try to identify a place in the call hierarchy that can handle the Exception instance, i.e., an intermediate method in the call hierarchy capable of handling either the specific exception or any of its parent types. For example: a method capable of handling IOException will take all the FileNotFoundException exceptions as well.

Consider the following example:

public static void main(String[] args){
   ExceptionHandling eh = new ExceptionHandling();
   eh.logInputAndCalculateArea(Double.parseDouble(args[0]), Double.parseDouble(args[1]));
}

private Optional<Double> logInputAndCalculateArea(Double length, Double breadth){
   Double area = null;
   try{
     logger.info("Dimensions are: [length: {}, breadth: [{}]", length, breadth);
     area = calculateArea(length, breadth);
   }catch (Exception exception){
     logger.error("exception occurred while calculating the area", exception);
   }
   return Optional.ofNullable(area);
}

private Double calculateArea(Double length, Double breadth){
    validate(length);
    validate(breadth);

    return length * breadth;
}

private void validate(Double value){
    if(null == value || value.compareTo((double) 0) < 0){
       throw new IllegalArgumentException("invalid value specified");
    }
}

In the absence of an exception handler in the calculateArea method, the IllegalArgumentException from method validate will be forwarded to logInputAndCalculateArea method - incase an invalid input is passed.

Exception Handling example scenario

In case no exception handler is found in the complete call hierarchy, the exception instance is forwarded to the default exception handler, and the program terminates abruptly.

Now, one thing is clear from the details mentioned above: a java method will only allow a single exception to be thrown at any given point. But there could be multiple exceptions from a single method; how do we ensure that correct details are propagated to the client code?

Consider another example:

// FileNotFoundException is hidden from the client. 
public void exceptionBearingAction(String path) throws IOException {
  BufferedReader reader = null;
  try{
    reader = new BufferedReader(new FileReader(path)); //FNF
  }finally{
    reader.close(); // NPE
  }
}

In case of an invalid path, the try block will throw a FileNotFoundException and the finally block will throw a NullPointerException. In such cases, it is the exception thrown from the finally block that will reach the calling code. The issue in such cases is that the client cannot recover the original exception thrown from the try block that caused the finally block to fail.


Suppressed Exceptions

Java 1.7 introduced the concept of suppressed exceptions. A suppressed exception is an exception that is ignored to let another exception be thrown from the method. A new method addSuppressed was added to the Throwable class to append such exceptions to the current exception being thrown.

public void suppressedExceptionBearingAction(String path) throws IOException {
   BufferedReader reader = null;
   Exception optionalException = null;
   try {
       reader = new BufferedReader(new FileReader(path)); // FNF
   } catch (IOException ioException) {
       optionalException = ioException;
   } finally {
       try{
           reader.close(); // NPE
       }catch (NullPointerException npe) {
          if (null != optionalException) {
              npe.addSuppressed(optionalException);
          }
          throw npe;
       }
   }
}

As shown in the code mentioned above, we can append the optionalException to the NullPointerException instance. The client can then retrieve the original exception based on the requirement and can handle it accordingly. Here is a quick spock test to demonstrate this:

def "Test the presence of a suppressed exception"(){
    given: "Context is loaded"
    when: "an exception from both try and finally is thrown"

    suppressedExceptionsDemo.suppressedExceptionBearingAction("some-random-path")
    then: "exception is received"
    def npe = thrown(NullPointerException.class)
    and: "exception thrown from finally block contains a suppressed exception"
    npe.getSuppressed().length > 0
    and: "the suppressed exception is of type FileNotFoundException, i.e. the one thrown from try block"
    FileNotFoundException.class == npe.getSuppressed()[0].class
}

In addition to this, suppressed exceptions play an important role while working with try-with-resources blocks.

The close method of an AutoClosable class is treated as an implicit finally block when working with try-with-resources block. In case an exception is thrown from the try block, and another exception is thrown from the close method, it is the exception thrown from the try block that will reach the client, and the one thrown from the close method, is added as a suppressed exception by the runtime itself:

public class SuppressedExceptionsTryWithResources {
    public void tryReadingFile(String path) throws Exception {
        try (SomeAutoCloseableClass someAutoCloseableClass = new SomeAutoCloseableClass()) {
            someAutoCloseableClass.exceptionBearingAction("some-random-path");
        }
    }
}

class SomeAutoCloseableClass implements AutoCloseable {
    public void exceptionBearingAction(String path) throws IOException {
        BufferedReader reader = reader = new BufferedReader(new FileReader(path)); // FNF
        reader.readLine();
    }
    
    @Override
    public void close() throws Exception {
        throw new RuntimeException("SomeAutoCloseableClass: Exception thrown from Implicit finally block while closing");
    }
}

And here is another test case to confirm:

def "Test in case of multiple exceptions, the one thrown from try reaches the client"(){
    given: "Context is loaded"
    when: "an exception from both try and implicit finally is thrown"

    suppressedExceptionsDemo.tryReadingFile("some-random-path")
    then: "exception is received"
    def fnf = thrown(FileNotFoundException.class)
    and: "suppressed exceptions is present"
    fnf.getSuppressed().length != 0
    and: "the exception thrown from the close method is added to the suppressed list"
    fnf.getSuppressed()[0].class == RuntimeException.class
    and: "check the message"
    fnf.getSuppressed()[0].message == "SomeAutoCloseableClass: Exception thrown from Implicit finally block while closing"
}

That is all for this post. If you want to share any feedback, please drop me an email, or contact me on any of the social platforms. I’ll try to respond at the earliest. Also, please consider subscribing for regular updates.

You can check the code in the github repo.

Be notified of new posts. Subscribe to the RSS feed.