Exception handling in Java: Advanced features and types

how-to
19 Sep 202423 mins
JavaProgramming LanguagesSoftware Development

Java exception handling with stack traces, exception chaining, try-with-resources, final re-throw, and StackWalker.

Tactical diagram of soccer plays in white chalk on a blackboard.
Credit: Thomas Bethge / Shutterstock

The Java platform includes a variety of language features and library types for dealing with exceptions, which are divergences from expected program behavior. In my previous article, you learned about Java’s basic exception handling capabilities. This article introduces advanced capabilities, including stack traces, exception chaining, try-with-resources, multi-catch, final re-throw, and stack walking.

What you’ll learn in this Java tutorial

This tutorial introduces advanced capabilities for handling exceptions in your Java programs:

  • Stack traces and stack trace elements
  • Causes and exception chaining
  • Try-with-resources
  • Multi-catch (multiple catch blocks)
  • Final re-throw
  • StackWalker and the StackWalking API

Exception handling with stack traces

Each JVM thread (a path of execution) is associated with a stack that’s created when the thread is created. This data structure is divided into frames, which are data structures associated with method calls. For this reason, each thread’s stack is often referred to as a method-call stack.

A new frame is created each time a method is called. Each frame stores local variables, parameter variables (which hold arguments passed to the method), information for returning to the calling method, space for storing a return value, information that’s useful in dispatching an exception, and so on.

A stack trace is a report of the active stack frames at a certain point in time during a thread’s execution. Java’s Throwable class (in the java.lang package) provides methods to print a stack trace, fill in a stack trace, and access a stack trace’s elements.

download
Download the source code for examples in this tutorial. Created by Jeff Friesen.

Printing a stack trace

When the throw statement throws a throwable, it first looks for a suitable catch block in the executing method. If not found, it unwinds the method-call stack looking for the closest catch block that can handle the exception. If not found, the JVM terminates with a suitable message. You can see this action in Listing 1.

Listing 1. PrintStackTraceDemo.java (version 1)

import java.io.IOException;

public class PrintStackTraceDemo
{
   public static void main(String[] args) throws IOException
   {
      throw new IOException();
   }
}

Listing 1’s contrived example creates a java.io.IOException object and throws this object out of the main() method. Because main() doesn’t handle this throwable, and because main() is the top-level method, the JVM terminates with a suitable message. For this application, you would see the following message:

Exception in thread "main" java.io.IOException
	at PrintStackTraceDemo.main(PrintStackTraceDemo.java:7)

The JVM outputs this message by calling Throwable‘s void printStackTrace() method, which prints a stack trace for the invoking Throwable object on the standard error stream. The first line shows the result of invoking the throwable’s toString() method. The next line shows data previously recorded by fillInStackTrace() (discussed shortly).

The stack trace reveals the source file and line number where the throwable was created. In this case, it was created on Line 7 of the PrintStackTrace.java source file.

You can invoke printStackTrace() directly, typically from a catch block. For example, consider a second version of the PrintStackTraceDemo application.

Listing 2. PrintStackTraceDemo.java (version 2)

import java.io.IOException;

public class PrintStackTraceDemo
{
   public static void main(String[] args) throws IOException
   {
      try
      {
         a();
      }
      catch (IOException ioe)
      {
         ioe.printStackTrace();
      }
   }

   static void a() throws IOException
   {
      b();
   }

   static void b() throws IOException
   {
      throw new IOException();
   }
}

Listing 2 reveals a main() method that calls method a(), which calls method b(). Method b() throws an IOException object to the JVM, which unwinds the method-call stack until it finds main()‘s catch block, which can handle the exception. The exception is handled by invoking printStackTrace() on the throwable. This method generates the following output:

java.io.IOException
	at PrintStackTraceDemo.b(PrintStackTraceDemo.java:24)
	at PrintStackTraceDemo.a(PrintStackTraceDemo.java:19)
	at PrintStackTraceDemo.main(PrintStackTraceDemo.java:9)

printStackTrace() doesn’t output the thread’s name. Instead, it invokes toString() on the throwable to return the throwable’s fully-qualified class name (java.io.IOException), which is output on the first line. It then outputs the method-call hierarchy: the most-recently called method (b()) is at the top and main() is at the bottom.

Filling in a stack trace

Throwable declares a Throwable fillInStackTrace() method that fills in the execution stack trace. In the invoking Throwable object, it records information about the current state of the current thread’s stack frames. Consider Listing 3.

Listing 3. FillInStackTraceDemo.java (version 1)

import java.io.IOException;

public class FillInStackTraceDemo
{
   public static void main(String[] args) throws IOException
   {
      try
      {
         a();
      }
      catch (IOException ioe)
      {
         ioe.printStackTrace();
         System.out.println();
         throw (IOException) ioe.fillInStackTrace();
      }
   }

   static void a() throws IOException
   {
      b();
   }

   static void b() throws IOException
   {
      throw new IOException();
   }
}

The main difference between Listing 3 and Listing 2 is the catch block’s throw (IOException) ioe.fillInStackTrace(); statement. This statement replaces ioe‘s stack trace, after which the throwable is re-thrown. You should observe this output:

java.io.IOException
	at FillInStackTraceDemo.b(FillInStackTraceDemo.java:26)
	at FillInStackTraceDemo.a(FillInStackTraceDemo.java:21)
	at FillInStackTraceDemo.main(FillInStackTraceDemo.java:9)

Exception in thread "main" java.io.IOException
	at FillInStackTraceDemo.main(FillInStackTraceDemo.java:15)

Instead of repeating the initial stack trace, which identifies the location where the IOException object was created, the second stack trace reveals the location of ioe.fillInStackTrace().

fillInStackTrace() invokes a native method that walks down the current thread’s method-call stack to build the stack trace. This walk is expensive and can impact performance if it occurs too often.

If you run into a situation (perhaps involving an embedded device) where performance is critical, you can prevent the stack trace from being built by overriding fillInStackTrace(). Check out Listing 4.

Listing 4. FillInStackTraceDemo.java (version 2)

{
   public static void main(String[] args) throws NoStackTraceException
   {
      try
      {
         a();
      }
      catch (NoStackTraceException nste)
      {
         nste.printStackTrace();
      }
   }

   static void a() throws NoStackTraceException
   {
      b();
   }

   static void b() throws NoStackTraceException
   {
      throw new NoStackTraceException();
   }
}

class NoStackTraceException extends Exception
{
   @Override
   public synchronized Throwable fillInStackTrace()
   {
      return this;
   }
}

Listing 4 introduces NoStackTraceException. This custom checked exception class overrides fillInStackTrace() to return this—a reference to the invoking Throwable. This program generates the following output:

NoStackTraceException

Comment out the overriding fillInStackTrace() method and you’ll observe the following output:

NoStackTraceException
	at FillInStackTraceDemo.b(FillInStackTraceDemo.java:22)
	at FillInStackTraceDemo.a(FillInStackTraceDemo.java:17)
	at FillInStackTraceDemo.main(FillInStackTraceDemo.java:7)

Accessing a stack trace’s elements

At times, you’ll need to access a stack trace’s elements in order to extract details required for logging, identifying the source of a resource leak, and other purposes. The printStackTrace() and fillInStackTrace() methods don’t support this task, but java.lang.StackTraceElement and its methods were made for it.

The java.lang.StackTraceElement class describes an element representing a stack frame in a stack trace. Its methods can be used to return the fully-qualified name of the class containing the execution point represented by this stack trace element along with other useful information. Here are the main methods:

  • String getClassName() returns the fully-qualified name of the class containing the execution point represented by this stack trace element.
  • String getFileName() returns the name of the source file containing the execution point represented by this stack trace element.
  • int getLineNumber() returns the line number of the source line containing the execution point represented by this stack trace element.
  • String getMethodName() returns the name of the method containing the execution point represented by this stack trace element.
  • boolean isNativeMethod() returns true when the method containing the execution point represented by this stack trace element is a native method.

Another important method is the StackTraceElement[] getStackTrace() on the java.lang.Thread and Throwable classes. This method respectively returns an array of stack trace elements representing the invoking thread’s stack dump and provides programmatic access to the stack trace information printed by printStackTrace().

Listing 5 demonstrates StackTraceElement and getStackTrace().

Listing 5. StackTraceElementDemo.java (version 1)

import java.io.IOException;

public class StackTraceElementDemo
{
   public static void main(String[] args) throws IOException
   {
      try
      {
         a();
      }
      catch (IOException ioe)
      {
         StackTraceElement[] stackTrace = ioe.getStackTrace();
         for (int i = 0; i < stackTrace.length; i++)
         {
            System.err.println("Exception thrown from " + 
                               stackTrace[i].getMethodName() + " in class " + 
                               stackTrace[i].getClassName() + " on line " + 
                               stackTrace[i].getLineNumber() + " of file " + 
                               stackTrace[i].getFileName());
            System.err.println();
         }
      }
   }

   static void a() throws IOException
   {
      b();
   }

   static void b() throws IOException
   {
      throw new IOException();
   }
}

When you run this application, you’ll observe the following output:

Exception thrown from b in class StackTraceElementDemo on line 33 of file StackTraceElementDemo.java

Exception thrown from a in class StackTraceElementDemo on line 28 of file StackTraceElementDemo.java

Exception thrown from main in class StackTraceElementDemo on line 9 of file StackTraceElementDemo.java

Finally, we have the setStackTrace() method on Throwable. This method is designed for use by remote procedure call (RPC) frameworks and other advanced systems, allowing the client to override the default stack trace that’s generated by fillInStackTrace() when a throwable is constructed.

I previously showed how to override fillInStackTrace() to prevent a stack trace from being built. Instead, you could install a new stack trace by using StackTraceElement and setStackTrace(). Create an array of StackTraceElement objects initialized via the following constructor, and pass this array to setStackTrace():

StackTraceElement(String declaringClass, String methodName, String fileName, int lineNumber)

Listing 6 demonstrates StackTraceElement and setStackTrace().

Listing 6. StackTraceElementDemo.java (version 2)

public class StackTraceElementDemo
{
   public static void main(String[] args) throws NoStackTraceException
   {
      try
      {
         a();
      }
      catch (NoStackTraceException nste)
      {
         nste.printStackTrace();
      }
   }

   static void a() throws NoStackTraceException
   {
      b();
   }

   static void b() throws NoStackTraceException
   {
      throw new NoStackTraceException();
   }
}

class NoStackTraceException extends Exception
{
   @Override
   public synchronized Throwable fillInStackTrace()
   {
      setStackTrace(new StackTraceElement[]
                    {
                       new StackTraceElement("*StackTraceElementDemo*", 
                                             "b()",
                                             "StackTraceElementDemo.java",
                                             22),
                       new StackTraceElement("*StackTraceElementDemo*", 
                                             "a()",
                                             "StackTraceElementDemo.java",
                                             17),
                       new StackTraceElement("*StackTraceElementDemo*", 
                                             "main()",
                                             "StackTraceElementDemo.java",
                                             7)
                    });
      return this;
   }
}

I’ve surrounded the StackTraceElementDemo class name with asterisks to prove that this stack trace is the one being output. Run the application and you’ll observe the following stack trace:

NoStackTraceException
	at *StackTraceElementDemo*.b()(StackTraceElementDemo.java:22)
	at *StackTraceElementDemo*.a()(StackTraceElementDemo.java:17)
	at *StackTraceElementDemo*.main()(StackTraceElementDemo.java:7)

Causes and exception chaining

A catch block often responds to an exception by throwing another exception. The first exception is said to cause the second exception to occur. Furthermore, the exceptions are said to be implicitly chained together.

For example, a library method’s catch block might be invoked with an internal exception that shouldn’t be visible to the library method’s caller. Because the caller needs to be notified that something went wrong, the catch block creates an external exception that conforms to the library method’s contract interface, and which the caller can handle.

Because the internal exception might be helpful in diagnosing the problem, the catch block should be able to wrap the internal exception in the external exception. The wrapped exception is known as a cause because its existence causes the external exception to be thrown. Also, the wrapped internal exception (cause) is explicitly chained to the external exception.

Support for causes and exception chaining is provided via two Throwable constructors (along with Exception, RuntimeException, and Error counterparts) and a pair of methods:

  • Throwable(String message, Throwable cause)
  • Throwable(Throwable cause)
  • Throwable getCause()
  • Throwable initCause(Throwable cause)

The Throwable constructors (and their subclass counterparts) let you wrap the cause while constructing a throwable. If you’re dealing with legacy code whose custom exception classes don’t support either constructor, you can wrap the cause by invoking initCause() on the throwable. Note that initCause() can be called just one time. Either way, you can return the cause by invoking getCause(). This method returns null when there is no cause.

Listing 7 presents a CauseDemo application that demonstrates causes (and exception chaining).

Listing 7. CauseDemo.java

public class CauseDemo
{
   public static void main(String[] args)
   {
      try
      {
         Library.externalOp();
      } 
      catch (ExternalException ee)
      {
         ee.printStackTrace();
      }
   }
}

class Library
{
   static void externalOp() throws ExternalException
   {
      try
      {
         throw new InternalException();
      } 
      catch (InternalException ie)
      {
         throw (ExternalException) new ExternalException().initCause(ie);
      }
   }

   private static class InternalException extends Exception
   {
   }
}

class ExternalException extends Exception
{
}

Listing 7 reveals CauseDemo, Library, and ExternalException classes. CauseDemo‘s main() method invokes Library‘s externalOp() method and catches its thrown ExternalException object. The catch block invokes printStackTrace() to output the external exception and its cause.

Library‘s externalOp() method deliberately throws an InternalException object, which its catch block maps to an ExternalException object. Because ExternalException doesn’t support a constructor that can take a cause argument, initCause() is used to wrap the InternalException object.

Run this application and you’ll observe the following stack traces:

ExternalException
	at Library.externalOp(CauseDemo.java:26)
	at CauseDemo.main(CauseDemo.java:7)
Caused by: Library$InternalException
	at Library.externalOp(CauseDemo.java:22)
	... 1 more

The first stack trace shows that the external exception originated in Library‘s externalOp() method (line 26 in CauseDemo.java) and was thrown out of the call to this method on line 7. The second stack trace shows that the internal exception cause originated in Library‘s externalOp() method (line 22 in CauseDemo.java). The ... 1 more line refers to the first stack trace’s final line. If you could remove this line, you’d observe the following output:

ExternalException
	at Library.externalOp(CauseDemo.java:26)
	at CauseDemo.main(CauseDemo.java:7)
Caused by: Library$InternalException
	at Library.externalOp(CauseDemo.java:22)
	at CauseDemo.main(CauseDemo.java:7)

You can prove that the second trace’s final line is a duplicate of the first stack trace’s final line by changing ee.printStackTrace(); to ee.getCause().printStackTrace();.

More about ‘…more’ and interrogating a chain of causes

In general, a ... n more line indicates that the last n lines of the cause’s stack trace are duplicates of the last n lines of the previous stack trace.

My example revealed only one cause. Exceptions thrown from non-trivial real-world applications may contain extensive chains of many causes. You can access these causes by employing a loop such as the following:

catch (Exception exc)
{
   Throwable t = exc.getCause();
   while (t != null)
   {
      System.out.println(t);
      t = t.getCause();
   }
}

Try-with-resources

Java applications often access files, database connections, sockets, and other resources that depend on related system resources (e.g., file handles). The scarcity of system resources implies that they must eventually be released, even when an exception occurs. When system resources aren’t released, the application eventually fails when attempting to acquire other resources, because no more related system resources are available.

In my introduction to exception handling basics, I mentioned that resources (actually, the system resources on which they depend) are released in a finally block. This can lead to tedious boilerplate code, such as the file-closing code that appears below:

finally
{
   if (fis != null)
      try
      {
         fis.close();
      }
      catch (IOException ioe)
      {
         // ignore exception
      }
   if (fos != null)
      try
      {
         fos.close();
      }
      catch (IOException ioe)
      {
         // ignore exception
      }
}

Not only does this boilerplate code add bulk to a classfile, the tedium of writing it might lead to a bug, perhaps even failing to close a file. JDK 7 introduced try-with-resources to overcome this problem.

The basics of try-with-resources

The try-with-resources construct automatically closes open resources when execution leaves the scope in which they were opened and used, whether or not an exception is thrown from that scope. This construct has the following syntax:

try (resource acquisitions)
{
   // resource usage
}

The try keyword is parameterized by a semicolon-separated list of resource-acquisition statements, where each statement acquires a resource. Each acquired resource is available to the body of the try block, and is automatically closed when execution leaves this body. Unlike a regular try statement, try-with-resources doesn’t require catch blocks and/or a finally block to follow try(), although they can be specified.

Consider the following file-oriented example:

try (FileInputStream fis = new FileInputStream("abc.txt"))
{
   // Do something with fis and the underlying file resource.
}

In this example, an input stream to an underlying file resource (abc.txt) is acquired. The try block does something with this resource, and the stream (and file) is closed upon exit from the try block.

Using ‘var’ with ‘try-with-resources’

JDK 10 introduced support for var, an identifier with special meaning (i.e., not a keyword). You can use var with try-with-resources to reduce boilerplate. For example, you could simplify the previous example to the following:

try (var fis = new FileInputStream("abc.txt"))
{
   // Do something with fis and the underlying file resource.
}

Copying a file in a try-with-resources context

In the previous article, I excerpted the copy() method from a file-copy application. This method’s finally block contains the file-closing boilerplate presented earlier. Listing 8 improves this method by using try-with-resources to handle the cleanup.

Listing 8. Copy.java

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class Copy
{
   public static void main(String[] args)
   {
      if (args.length != 2)
      {
         System.err.println("usage: java Copy srcfile dstfile");
         return;
      }

      try
      {
         copy(args[0], args[1]);
      }
      catch (IOException ioe)
      {
         System.err.println("I/O error: " + ioe.getMessage());
      }
   }

   static void copy(String srcFile, String dstFile) throws IOException
   {
      try (FileInputStream fis = new FileInputStream(srcFile);
           FileOutputStream fos = new FileOutputStream(dstFile))
      {
         int c;
         while ((c = fis.read()) != -1)
            fos.write(c);
      }
   }
}

copy() uses try-with-resources to manage source and destination file resources. The round bracketed-code following try attempts to create file input and output streams to these files. Assuming success, its body executes, copying the source file to the destination file.

Whether an exception is thrown or not, try-with-resources ensures that both files are closed when execution leaves the try block. Because the boilerplate file-closing code that was shown earlier isn’t needed, Listing 8’s copy() method is much simpler and easier to read.

Designing resource classes to support try-with-resources

The try-with-resources construct requires that a resource class implement the java.lang.Closeable interface or the JDK 7-introduced java.lang.AutoCloseable superinterface. Pre-Java 7 classes like java.io.FileInputStream implemented Closeable, which offers a void close() method that throws IOException or a subclass.

Starting with Java 7, classes can implement AutoCloseable, whose single void close() method can throw java.lang.Exception or a subclass. The throws clause has been expanded to accommodate situations where you might need to add close() methods that can throw exceptions outside of the IOException hierarchy; for example, java.sql.SQLException.

Listing 9 presents a CustomARM application that shows you how to configure a custom resource class so that you can use it in a try-with-resources context.

Listing 9. CustomARM.java

public class CustomARM
{
   public static void main(String[] args)
   {
      try (USBPort usbp = new USBPort())
      {
         System.out.println(usbp.getID());
      }
      catch (USBException usbe)
      {
         System.err.println(usbe.getMessage());
      }
   }
}

class USBPort implements AutoCloseable
{
   USBPort() throws USBException
   {
      if (Math.random() < 0.5)
         throw new USBException("unable to open port");
      System.out.println("port open");
   }

   @Override
   public void close()
   {
      System.out.println("port close");
   }

   String getID()
   {
      return "some ID";
   }
}

class USBException extends Exception
{
   USBException(String msg)
   {
      super(msg);
   }
}

Listing 9 simulates a USB port in which you can open and close the port and return the port’s ID. I’ve employed Math.random() in the constructor so that you can observe try-with-resources when an exception is thrown or not thrown.

Compile this listing and run the application. If the port is open, you’ll see the following output:

port open
some ID
port close

If the port is closed, you might see this:

unable to open port

Suppressing exceptions in try-with-resources

If you’ve had some programming experience, you might have noticed a potential problem with try-with-resources: Suppose the try block throws an exception. This construct responds by invoking a resource object’s close() method to close the resource. However, the close() method might also throw an exception.

When close() throws an exception (e.g., FileInputStream‘s void close() method can throw IOException), this exception masks or hides the original exception. It seems that the original exception is lost.

In fact, this isn’t the case: try-with-resources suppresses close()‘s exception. It also adds the exception to the original exception’s array of suppressed exceptions by invoking java.lang.Throwable‘s void addSuppressed(Throwable exception) method.

Listing 10 presents a SupExDemo application that demonstrates how to repress an exception in a try-with-resources context.

Listing 10. SupExDemo.java

import java.io.Closeable;
import java.io.IOException;

public class SupExDemo implements Closeable
{
   @Override
   public void close() throws IOException
   {
      System.out.println("close() invoked");
      throw new IOException("I/O error in close()");
   }

   public void doWork() throws IOException
   {
      System.out.println("doWork() invoked");
      throw new IOException("I/O error in work()");
   }

   public static void main(String[] args) throws IOException
   {
      try (SupExDemo supexDemo = new SupExDemo())
      {
         supexDemo.doWork();
      }
      catch (IOException ioe)
      {
         ioe.printStackTrace();
         System.out.println();
         System.out.println(ioe.getSuppressed()[0]);
      }
   }
}

Listing 10’s doWork() method throws an IOException to simulate some kind of I/O error. The close() method also throws the IOException, which is suppressed so that it doesn’t mask doWork()‘s exception.

The catch block accesses the suppressed exception (thrown from close()) by invoking Throwable‘s Throwable[] getSuppressed() method, which returns an array of suppressed exceptions. Only the first element is accessed because only one exception is suppressed.

Compile Listing 10 and run the application. You should observe the following output:

doWork() invoked
close() invoked
java.io.IOException: I/O error in work()
        at SupExDemo.doWork(SupExDemo.java:16)
        at SupExDemo.main(SupExDemo.java:23)
        Suppressed: java.io.IOException: I/O error in close()
                at SupExDemo.close(SupExDemo.java:10)
                at SupExDemo.main(SupExDemo.java:24)

java.io.IOException: I/O error in close()

Multiple catch blocks (multi-catch)

Starting in JDK 7, it is possible to codify a single catch block that catches more than one type of exception. The purpose of this multi-catch feature is to reduce code duplication and reduce the temptation to catch overly broad exceptions (for instance, catch (Exception e)).

Suppose you’ve developed an application that gives you the flexibility to copy data to a database or file. Listing 11 presents a CopyToDatabaseOrFile class that simulates this situation, and demonstrates the problem with catch block code duplication.

Listing 11. CopyToDatabaseOrFile.java

import java.io.IOException;

import java.sql.SQLException;

public class CopyToDatabaseOrFile
{
   public static void main(String[] args)
   {
      try
      {
         copy();
      }
      catch (IOException ioe)
      {
         System.out.println(ioe.getMessage());
         // additional handler code
      }
      catch (SQLException sqle)
      {
         System.out.println(sqle.getMessage());
         // additional handler code that's identical to the previous handler's
         // code
      }
   }

   static void copy() throws IOException, SQLException
   {
      if (Math.random() < 0.5)
         throw new IOException("cannot copy to file");
      else
         throw new SQLException("cannot copy to database");
   }
}

JDK 7 overcomes this code duplication problem by letting you specify multiple exception types in a catch block where each successive type is separated from its predecessor by placing a vertical bar (|) between these types:

try
{
   copy();
}
catch (IOException | SQLException iosqle)
{
   System.out.println(iosqle.getMessage());
}

Now, when copy() throws either exception, the exception will be caught and handled by the catch block.

When multiple exception types are listed in a catch block’s header, the parameter is implicitly regarded as final. As a result, you cannot change the parameter’s value. For example, you cannot change the reference stored in the previous code fragment’s iosqle parameter.

Final re-throw

Starting in JDK 7, the Java compiler is able to analyze re-thrown exceptions more precisely than in previous Java versions. This feature only works when no assignments are made to a re-thrown exception’s catch block parameter, which is considered to be effectively final. When a preceding try block throws an exception that’s a supertype/subtype of the parameter’s type, the compiler throws the caught exception’s actual type instead of throwing the parameter’s type (as was done in previous Java versions).

The purpose of this final re-throw feature is to facilitate adding a trycatch statement around a block of code to intercept, process, and re-throw an exception without affecting the statically determined set of exceptions thrown from the code. Also, this feature lets you provide a common exception handler to partly handle the exception close to where it’s thrown, and provide more precise handlers elsewhere that handle the re-thrown exception. Consider Listing 12.

Listing 12. MonitorEngine.java

class PressureException extends Exception
{
   PressureException(String msg)
   {
      super(msg);
   }
}

class TemperatureException extends Exception
{
   TemperatureException(String msg)
   {
      super(msg);
   }
}

public class MonitorEngine
{
   public static void main(String[] args)
   {
      try
      {
         monitor();
      }
      catch (Exception e)
      {
         if (e instanceof PressureException)
            System.out.println("correcting pressure problem");
         else
            System.out.println("correcting temperature problem");
      }
   }

   static void monitor() throws Exception
   {
      try
      {
         if (Math.random() < 0.1)
            throw new PressureException("pressure too high");
         else
         if (Math.random() > 0.9)
            throw new TemperatureException("temperature too high");
         else
            System.out.println("all is well");
      }
      catch (Exception e)
      {
         System.out.println(e.getMessage());
         throw e;
      }
   }
}

Listing 12 simulates the testing of an experimental rocket engine to see if the engine’s pressure or temperature exceeds a safety threshold. It performs this testing via the monitor() helper method.

monitor()‘s try block throws PressureException when it detects a pressure extreme, and throws TemperatureException when it detects a temperature extreme. (Because this is only a simulation, random numbers are used — the java.lang.Math class’s static double random() method returns a random number between 0.0 and (almost) 1.0.) The try block is followed by a catch block designed to partly handle the exception by outputting a warning message. This exception is then re-thrown so that monitor()‘s calling method can finish handling the exception.

Before JDK 7 you couldn’t specify PressureException and TemperatureException in monitor()‘s throws clause because the catch block’s e parameter is of type java.lang.Exception and re-throwing an exception was treated as throwing the parameter’s type. JDK 7 and successor JDKs have made it possible to specify these exception types in the throws clause because their compilers can determine that the exception thrown by throw e comes from the try block, and only PressureException and TemperatureException can be thrown from this block.

Because you can now specify static void monitor() throws PressureException, TemperatureException, you can provide more precise handlers where monitor() is called, as the following code fragment demonstrates:

try
{
   monitor();
}
catch (PressureException pe)
{
   System.out.println("correcting pressure problem");
}
catch (TemperatureException te)
{
   System.out.println("correcting temperature problem");
}

Because of the improved type checking offered by final re-throw in JDK 7, source code that compiled under previous versions of Java might fail to compile under later JDKs. For example, consider Listing 13.

Listing 13. BreakageDemo.java

class SuperException extends Exception
{
}

class SubException1 extends SuperException
{
}

class SubException2 extends SuperException
{
}

public class BreakageDemo
{
   public static void main(String[] args) throws SuperException
   {
      try
      {
         throw new SubException1();
      }
      catch (SuperException se)
      {
         try
         {
            throw se;
         }
         catch (SubException2 se2)
         {
         }
      }
   }
}

Listing 13 compiles under JDK 6 and earlier. However, it fails to compile under later JDKs, whose compilers detect and report the fact that SubException2 is never thrown in the body of the corresponding try statement. This is a small problem that you are unlikely to encounter in your programs, and a worthwhile trade-off for having the compiler detect a source of redundant code. Removing redundancies results in cleaner code and smaller classfiles.

StackWalker and the StackWalking API

Obtaining a stack trace via Thread‘s or Throwable‘s getStackTrace() method is costly and impacts performance. The JVM eagerly captures a snapshot of the entire stack (except for hidden stack frames), even when you only need the first few frames. Also, your code will probably have to process frames that are of no interest, which is also time-consuming. Finally, you cannot access the actual java.lang.Class instance of the class that declared the method represented by a stack frame. To access this Class object, you’re forced to extend java.lang.SecurityManager to access the protected getClassContext() method, which returns the current execution stack as an array of Class objects.

JDK 9 introduced the java.lang.StackWalker class (with its nested Option class and StackFrame interface) as a more performant and capable alternative to StackTraceElement (plus SecurityManager). To learn about StackWalker and its related types, see my introduction to the StackWalking API.

In conclusion

This article completes my two-part introduction to Java’s exception handling framework. You might want to reinforce your understanding of this framework by reviewing Oracle’s Exceptions lesson in the Java Tutorials. Another good resource is Baeldung’s Exception handling in Java tutorial, which includes anti-patterns in exception handling.

Exit mobile version