Artikkelit
Exceptions - the big picture
12.11.2008
Author Marko Oikarinen
Out there in real projects we encounter a lot of code that is unnecessarily complex or outright buggy because of too much exception handling code. This article focuses on strategic exception handling and presents general guidelines on what and where to catch and throw.
Introduction
Exceptions promise to free your code from excessive error handling logic thus making it easier to understand what the code does. Yet, out there in real projects we encounter a lot of code that is unnecessarily complex or outright buggy because of too much exception handling code. Maybe this is because of those code snippets from books and articles that state “error handling is left as an exercise for the reader” or “this is not production quality code - error handling omitted”. They seem to imply that all your methods need some error handling code.
There is a lot of reading material available on the low level mechanics of exception handling in various programming languages. This article focuses on the big picture and presents general guidelines on what and where to catch and throw. These concepts are not language specific - I have used them in Java, C# and C++. They also apply to many other languages that have similar exception behavior. The reader is assumed to have a basic understanding on exceptions and to know what try, catch and throw mean.
Catch strategy
Recently, I read a coding conventions document of a medium-size enterprise project. It stated something to the effect that “exceptions should be handled as early as possible”. I read the word early here as a synonym for close to the source. What's interesting is that this advice is totally opposite to what I do!
public Payload readMessage(Message msg) {
try {
Header header = parseHeader(msg); // throws
Body body = parseBody(msg);
return new Payload(header.getMessageId(), body);
} catch (HeaderChecksumMismatchException e) {
ErrorDialog.show("Invalid message header");
return null;
}
}The parseHeader() method may throw a HeaderChecksumMismatchException. It gets catched in the method that directly calls parseHeader(). That's pretty close to the source, to get any closer we'd have to catch it in the method that throws the exception. The exception is handled by showing an error dialog and returning null to the caller.
What's the calling method going to do with the return value from readMessage()? Likely it will check if it's not null and do some work with the payload. What if it is null? Well, do error handling.
Now, wait a minute! Wasn't the error already handled - as early as possible like the coding conventions recommend? It turns out that trying to get rid of exceptions too soon ends up complicating the code with alternative error reporting mechanisms like returning a boolean, a status code or null.
I recommend that if not catching an exception at the method your looking at ends up doing the right thing, don't catch. To handle exceptions you need only a few strategically placed catch clauses in your application - nine times out of ten they get the job done. Any unfinished work gets rolled back, the user sees a friendly error dialog and the code is more robust.
Cover your entry points
We would like to put in place a safety net that ensures that wherever things might go wrong we know it. What's the minimum amount of code we need to make sure that no exceptions leak through unnoticed? Depending on the kind of program you're writing, there are a couple of different things you might have to do. The basic principle underlying each of the cases is the same: cover all entry points to your code with a try/catch block.
The simplest thing that could possibly work - main()
The most obvious entry point in a program is the main() function. In a UNIX-style command line application that does a well defined job and then exits, this is the place where all exceptions ultimately end up unless tampered with.
Here's the main() of the open source dependency analysis tool dtangler [1]:
public static void main(String[] args) {
try {
printVersionInfo();
CommandLineApp app = new CommandLineApp(new SysoutWriter());
boolean returnSuccess = app.run(args);
if (returnSuccess) System.exit(0);
else System.exit(-1);
} catch (DtException e) {
System.err.println("Error: " + e.getMessage());
} catch (Exception e) {
System.err.println("Internal error: " + e.getMessage());
e.printStackTrace();
}
System.exit(-2);
}Everything the program does happens inside this try/catch block - there is no way an error gets unnoticed. I'll explain why there are two catch clauses later on.
GUI frameworks
Most graphical user interface frameworks like being in control. The application code does some setup work and relinquishes control to the GUI frameworks event loop. When the event loop has started, nothing happens unless there is an event to handle. Don't call us, we'll call you, right?
When we get the call from the GUI framework it dispatches to event handling code written by us. In other words, the event handler is an entry point to application code and needs to be covered by a try/catch block:
void onClickSave(ClickEventArgs a) {
try {
model.save();
view.refresh();
} catch(Exception e) {
NiceErrorDialog.show(e);
}
}
void onClickCancel(ClickEventArgs a) {
try {
model.cancel();
view.close();
} catch(Exception e) {
NiceErrorDialog.show(e);
}
}
void onClickPrint(ClickEventArgs a) {
try {
model.print();
} catch(Exception e) {
NiceErrorDialog.show(e);
}
}Ugh… that's a lot of duplicated code. Do we really have to add that piece of boiler-plate code to the gazillion event handlers in our application? Luckily, many GUI frameworks provide a centralized mechanism for catching exceptions thrown by event handlers, so you usually don't need to put try/catch around everything you do.
For example, Java/Swing supports centralized exception handling. Here is the start up code of the dtangler Swing UI:
String handlerClassName = UIExceptionHandlerDelegator.class.getName();
System.setProperty("sun.awt.exception.handler", handlerClassName);
SwingWindowManager windowManager = new SwingWindowManager();
UIExceptionHandlerDelegator.setUIExceptionHandler(windowManager);We tell Swing that we want to use our exception handler by setting the system property 'sun.awt.exception.handler'. If the Swing event loop encounters an exception, it is passed to our UIExceptionHandlerDelegator. The delegator passes the exception to our window manager which is responsible for displaying a nice error dialog.
Threads
Threads present a barrier to exceptions - exceptions do not have the ability to jump from a thread to another - at least not without outside assistance. To keep our safety net coverage we need to catch exceptions at the thread entry point.
In Java, we put our code in the run()-method of a Runnable and hand that over to a Thread-object:
class GizmoTask implements Runnable {
public void run() {
try {
ourCodeThatDoesFancyStuff();
} catch(Exception e) {
errorLog.onError(e);
// or communicate the error to the calling thread
// or ... whatever is appropriate...
}
}
public static void main(String[] args) {
try {
new Thread(new GizmoTask()).start();
// ...main thread does something else here ...
} catch(Exception e) {
errorLog.onError(e);
System.exit(-42);
}
}
}If we omit the try/catch in GizmoTask.run() the exceptions thrown by ourCodeThatDoesFancyStuff() get lost forever. Our error log would contain no trace of any problems originating from GizmoTask. Java does print the stack trace of any uncatched exceptions to the standard error output but that is rarely enough.
There is no default error handling mechanism for threads that does the right thing. You absolutely need to figure out a strategy for dealing with exceptions at the thread entry point. How you report the error and whether you keep going or exit the thread depends on the scenario.
Task-oriented processing
Now that we have our safety net in place and our program knows how to die gracefully, let's consider a more complex application. Here's example code from a server application that processes messages from a client. Each message should be processed in a transaction. In case of a processing failure the server should not die - it just has to roll back the transaction for the current message and keep listening for new messages.
public void serve() {
while (true) {
Message input = null;
try {
input = waitForClientMessage();
if(input.equals(Message.QUIT)) break;
beginTransaction();
process(input);
commitTransaction();
} catch (Exception e) {
rollbackTransaction();
logError("Problem processing message", input, e);
}
}
}It is pretty common for an application to do its processing with independent tasks or units of work. They are independent in the sense that if a single task fails, the application still continues to process more tasks. These tasks may come in all kinds of forms - for example some programs use line-oriented processing on text input where each line is treated as an independent unit of work. The GUI code we looked at before has the same structure. Each event handler does a single unit of work and we keep handling events even if one of them fails.
When dealing with units of work it is common to have to transform the exception to another error reporting mechanism:
} catch (Exception e) {
rollbackTransaction();
logError("Problem processing message", inputXml, e);
return buildXmlErrorMessage(e);
}As you design your application, consider if you have situations that call for task-oriented processing. Most server applications work this way. Though a task is often executed inside a transaction, you can have task-oriented processing without transactions.
Add information - don't throw it away
Having analyzed some largish decent quality code bases, I've come to the conclusion that the most common reason to catch an exception is to add some context information and pass it along. For example, a raw file not found exception message looks like this: ” java.io.FileNotFoundException: dtangler.txt”. The user has a better chance of understanding the problem if we tell that the file we are looking for is a configuration file and we expect to find it from /home/joe/dtangler/. Here's the actual example from dtangler source:
private static FileInputStream openFile(File configFile) {
try {
return new FileInputStream(configFile);
} catch (FileNotFoundException e) {
throw new DtException("config file not found: "
+ configFile.getAbsolutePath(), e);
}
}When you add context information this way, never ever lose the original cause! Use the exception chaining mechanism to ensure that wherever the exception ends up, all the information gathered along the way is available.
Exception categories
Let's revisit dtangler main():
public static void main(String[] args) {
try {
...
} catch (DtException e) {
System.err.println("Error: " + e.getMessage());
} catch (Exception e) {
System.err.println("Internal error: " + e.getMessage());
e.printStackTrace();
}What's the big idea here - why have two catch clauses where one prints the stack trace and the other one does not? DtException is what some people call an application exception. We want to distinguish everyday minor problems where the Problem Exists Between Keyboard And Chair from errors that indicate a bug in our software. Here's an example of an application exception:
public void setLiteral(String literal) {
if (literal.contains("@")) throw new DtException(
"Item name cannot contain the character '@'");
value = literal;
}The user is supposed to know that you cannot use the '@' character in names of groups and other items (it's in the manual). Sometimes the user tries to use the character anyway. We consider this normal and tell the user that he did something unexpected: “Error: Item name cannot contain the character '@'”. What we do NOT want to do is dump a huge stack trace in your face if you do a simple mistake. Use application exceptions for errors that are part of the problem domain. These are meaningful to the user and should be reported in a friendly way.
In the rare case that there really is a bug and we get a null pointer exception, we want that stack trace because it helps us find the bug and squash it. In dtangler main() any exception that is not a DtException ends up in the second catch block and gets reported as an internal error. A non-technical user does not really care or understand what exactly went wrong so we want to make it clear that this is a technical problem and not the users fault.
In larger systems we may need additional error categories - for example for environmental problems such as network failures and server maintenance breaks.
Writing new exception types
When do you need to write a new exception class? If your code is for internal use (i.e. inside your project/team) you know that you need a new exception type if there is a specific error condition that calls for custom processing:
try {
server.processOrder(order);
} catch (Exception e) {
if(e.getMessage().contains("order blocked")) {
sendToBlockedQueue(order);
}
else throw e; // some other error
}The code gets cleaner if the server API has an exception type for the 'order is blocked' condition. We can catch the specific error we are interested in and we do not need re-throw logic:
try {
server.processOrder(order);
} catch (OrderBlockedException e) {
sendToBlockedQueue(order);
}Adding new exception types on demand works fine if you own the code of both the caller and method that gets called. If you are writing a library for others to use, you need to consider which error conditions (or categories of errors) the users of your library are likely to want to handle in a 'non-standard' way. Implement specific exception types for errors the caller likely wants to write a separate catch-clause for.
Exceptions and package dependencies
So, you got a new exception type. Which package does it belong to? Let's look at the dependencies we have:
public class OrderBlockedException extends RuntimeException {
public OrderBlockedException(String message, Throwable cause) {
super(message, cause);
}
}The exception class depends on RuntimeException, String and Throwable which are all in package java.lang. Those are in the JDK and available to all java code - in effect we have no interesting dependencies.
public interface OrderServer {
DeliverySpec processOrder(Order o) throws OrderBlockedException;
} A call to OrderServer.processOrder() might throw an OrderBlockedException instead of returning a DeliverySpec. OrderBlockedException can be seen as an alternative return value. The four classes Order, DeliverySpec, OrderBlockedException and OrderServer are what you need to work with order processing in this application. These classes are used together - putting all of them in the same package gives us good cohesion and honors the Common-Reuse Principle [CRP]. Moving any one of them to another package containing classes unrelated to order processing would introduce a new potentially harmful dependency between packages.
Sometimes you see all the exception classes of an application bundled together in a dedicated exceptions package (eg.foo.exceptions). This is a bad idea. If our order processing changes in a way that OrderBlockedException needs to reference the actual order object - all other unrelated code that uses exceptions gains a dependency on the Order class!
In general, deciding the package structure should be based on dependencies between classes - not on superficial similarity or role of the classes. Would you populate a project team with all managers and put the developers on a separate team?
Finally
The bulk of the program code should not have to deal with exceptions. Exceptions enable you to code a clean happy path and deal with problems at the edges - at entry points and low level routines.
As an example we can examine the core business logic of dtangler - the classes under org.dtangler.core. There are catch clauses only in main() and in the package org.dtangler.core.input which handles configuration files and command line arguments. The other 13 packages containing 51 classes have NO exception-related code at all! There is not a single try/catch block in there. The code is on the “happy path” and easy to read.
Using exceptions is easy - just remember the main three points:
* When throwing add enough information (and never ever lose the original cause)
* Don't let exceptions vanish in thin air - cover your entry points
* Figure out what your sub tasks/units of work are and deal with exceptions there
References
[1] dtangler - the open source dependency analysis tool - www.dtangler.org
[2] The Common-Reuse Principle (CRP). Robert C. Martin / Agile software Development - Principles, Patterns and Practices. chapter 20: Principles of Package Design