Tapestry Training -- From The Source

Let me help you get your team up to speed in Tapestry ... fast. Visit howardlewisship.com for details on training, mentoring and support!

Thursday, July 24, 2003

Thoughts on Exceptions

Everyone has their own approach to exceptions. Here's a few of mine.

Runtime vs. Checked

Checked exceptions should be used only when they will be explicitly caught and code paths will diverge based on the type of exception. This is, in fact, extremely rare (and some JVM code violates this pattern). For example:

catch (FooFrobbedException ex)
  // recover

That's where you want to use a checked exception. Generally, these situations only occur along software boundaries, usually between the code-you-write and the code-you-got (from a library). Questions to ask: can this be recovered from? and will anyone catch this?

Catch and Wrap

This again occurs along boundaries; often you'll catch someone else's checked exception, and (since there is no recovery), rethrow it (in a runtime exception). What's essential is that you keep a reference to the original exception. I tend to call this a "root cause". Tapestry exceptions have been doing this forever; JDK 1.4 has added the ability to attach root cause exceptions consistently (in a somewhat ugly way to maintain backwards compatibility). In JDK 1.3 and earlier it was hit and miss.

Why is this important? Because as you catch and throw, you are losing information; a SAXParseException may identify the public id, line and column but as it goes up the stack, that information is lost; we lose the line and column when we say "can't parse foo.xml", and we may lose even that amount of context when we throw an exception that says "can't accomplish task foo".

Add information

If you are going to catch an exception and rethrow it, add information along the way. Extend the exception message with more information, to provide more context. Add additional read-only properties to the exception to identify more information. The point of this wrapping and rethrowing is that an uncaught, top-level exception provides you with the tools to identify the bug and fix the problem.

Log it or not?

In some application's I've worked on, every time an exception occurs, it gets logged as an ERROR. Of course, that exception gets wrapped and rethrown ... and caught, and logged and rethrown. The end result is thousands of lines of meaningless stack trace scrolling by, which only muddles things ... it doesn't help track down the problem or even the source.

The right approach: catch the exception, and log it, as DEBUG (or at most WARN) before wrapping and rethrowing it ... but only at code boundaries. These are those same code boundaries between, typically, your applications code and library and framework code you've got or bought. That's the only interesting place. Why not ERROR? Because you have top level reporting (next item).

Top level exception reporting

This is the most important part of exception handling ... what to do when nobody catches and handles the exception. It all comes together here, something Tapestry has excelled at since before 1.0. When a top-level exception makes it to the top loop (in Tapestry's case, to the Engine's service() method) you want to produce as much information as possible.

Tapestry's approach has been to identify the stack of exceptions, starting with the outer-most one and working inwards. Each exception's class name and message is displayed, along with all readable properties. Along the way, it finds the next innermost exception and does the same thing ... down, down, down to the deepest exception. That's where the stack trace is displayed.

Tapestry includes a utility class, ExceptionAnalyzer, for just this purpose (it may move to HiveMind in the future).

1 comment:

Anonymous said...

Hi Howard,

Any thoughts on having the ability to throw a "catchable" exception from the service (or the Domain) layer and then being able to wrap it as a "validation" message.

In a way using the exceptions to manage custom validations and writing one generic "validator" for it.

Unless there is a way to to this already in Tapestry of course...