Java exception handling with stack traces, exception chaining, try-with-resources, final re-throw, and StackWalker.
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.
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()
returnstrue
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 try
–catch
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.