Exception enrichment is an alternative to
exception wrapping.
Exception wrapping has a couple of disadvantages that exception enrichment can fix.
These disadvantages are:
- Exception wrapping may result in very long stack traces consisting of one
stack trace for each exception in the wrapping hierarchy. Most often
only the root stack trace is interesting. The rest of the stack traces are
then just annoying.
- The messages of the exceptions are spread out over the stack traces. The message
of an exception is typically printed above the stack trace. When several exceptions
wrap each other in a hierarchy, all these messages are spread out in between the
stack traces. This makes it harder to determine what went wrong, and what the
program was trying to do when the error happened. In other words, it makes it hard
to determine in what context the error occurred. The error might have occurred in a
PersonDao class, but was it called from a servlet or from a web service when
it failed?
In exception enrichment you do not wrap exceptions. Instead you add contextual information
to the original exception and rethrow it. Rethrowing an exception does not reset the
stack trace embedded in the exception.
Here is an example:
public void method2() throws EnrichableException{
try{
method1();
} catch(EnrichableException e){
e.addInfo("An error occurred when trying to ...");
throw e;
}
}
public void method1() throws EnrichableException {
if(...) throw new EnrichableException(
"Original error message");
}
As you can see the method1() throws an EnrichableException which is a superclass
for enrichable exceptions. This is not a standard Java exception, so you will have to
create it yourself. There is an example EnrichableException at the end of this text.
Notice how method2() calls the addInfo() method on the caught EnrichableException,
and rethrow it afterwards. As the exception propagates up the call stack, each
catch block can add relevant information to the exception if necessary.
Using this simple technique you only get a single stack trace, and still get any
relevant contextual information necessary to investigate the cause of the exception.
Unique Error Codes
It is sometimes a requirement that each error raised in an application is
identified by a unique error code. This can be a bit problematic since some
errors are raised inside components that are reused throughout the application.
Therefore an exception may seem the same to the component throwing it, but the
context in which the exception occurs is different.
Here is an example:
public void method3() throws EnrichableException{
try{
method1();
} catch(EnrichableException e){
e.addInfo("An error occurred when trying to ...");
throw e;
}
}
public void method2() throws EnrichableException{
try{
method1();
} catch(EnrichableException e){
e.addInfo("An error occurred when trying to ...");
throw e;
}
}
public void method1() throws EnrichableException {
if(...) throw new EnrichableException(
"ERROR1", "Original error message");
}
Notice how method1() adds the code "ERROR1" to the thrown EnrichableException
to uniquely identify that error cause. But notice too that method1() is
called from both method2() and method3(). Though the error may seem the same
to method1() no matter which of method2() and method3() that called it, this
may important to know for the developer investigating the error. The error code
"ERROR1" is enough to determine where the error occurred, but not in what context
it occurred.
A solution to this problem is to add unique context error codes to the exception
the same way the other contextual information is added. Here is an example where
the addInfo() method has been changed to accommodate this:
public void method3() throws EnrichableException{
try{
method1();
} catch(EnrichableException e){
e.addInfo("METHOD3", "ERROR1",
"An error occurred when trying to ...");
throw e;
}
}
public void method2() throws EnrichableException{
try{
method1();
} catch(EnrichableException e){
e.addInfo("METHOD2", "ERROR1",
"An error occurred when trying to ...");
throw e;
}
}
public void method1() throws EnrichableException {
if(...) throw new EnrichableException(
"METHOD1", "ERROR1", "Original error message");
}
Two new parameters have been added to the addInfo() method and the constructor
of the EnrichableException. The first parameter is a code identifying the
context in which the error occurred. The second parameter is a unique error code
within that context. An error identification for an exception thrown by method1() when
called from method2() will now look like this:
[METHOD2:ERROR1][METHOD1:ERROR1]
When method1() is called from method3() the error identification will look like this:
[METHOD3:ERROR1][METHOD1:ERROR1]
As you can see it is now possible to distinguish an exception thrown from
method1() via method2() from the same exception thrown from method1() via
method3().
You may not always need the extra contextual error codes, but when you do
the solution sketched in this section is an option.
Wrapping Non-Enrichable Exceptions
You may not always be able to avoid exception wrapping. If a component in your
application throws a checked exception that is not enrichable, you may have to
wrap it in an enrichable exception. Here is an example where method1() catches
a non-enrichable exception and wraps it in an enrichable exception, and throws
the enrichable exception:
public void method1() throws EnrichableException {
try{
... call some method that throws an IOException ...
} catch(IOException ioexception)
throw new EnrichableException( ioexception,
"METHOD1", "ERROR1", "Original error message");
}
Unchecked EnrichableException
I used to be in favor of checked exceptions but over the last couple of
years my opinion has changed. Now i feel that checked exceptions are more
of a nuisance than a help. Therefore I would prefer to make my
EnrichableException unchecked, by having it extend RuntimeException.
There is a more thorough discussion of checked and unchecked exceptions
in the text
"Checked vs. Unchecked Exceptions".
Exception Enrichment and Pluggable Exception Handlers
Like with any other exception type it is possible to use pluggable exception
handlers with enrichable exceptions. If you use the unique error
codes described earlier these codes must be added as paremeters to the
exception handler interface. Here is an example exception handler interface
supporting these unique error codes:
public interface ExceptionHandler{
public void handle(String contextCode, String errorCode,
String errorText, Throwable t)
public void raise(String contextCode, String errorCode,
String errorText);
}
Exceptions caught in the program will be passed to the handleException()
which will decide what concrete exception to throw instead. In this case
an EnrichableException is thrown. If the EnrichableException is unchecked
it is not necessary to declare it in the handleException() method.
An Example EnrichableException
Below is an example of an enrichable exception that you can use
as a template for your own enrichable exceptions. You may need to change
the class definition to suit your own needs. The exception is designed
to use unique error codes as described earlier in this text.
Below the code is an example application that uses the EnrichableException,
and a stack trace generated from this application.
import java.util.ArrayList;
import java.util.List;
public class EnrichableException extends RuntimeException {
public static final long serialVersionUID = -1;
protected List<InfoItem> infoItems =
new ArrayList<InfoItem>();
protected class InfoItem{
public String errorContext = null;
public String errorCode = null;
public String errorText = null;
public InfoItem(String contextCode, String errorCode,
String errorText){
this.errorContext = contextCode;
this.errorCode = errorCode;
this.errorText = errorText;
}
}
public EnrichableException(String errorContext, String errorCode,
String errorMessage){
addInfo(errorContext, errorCode, errorMessage);
}
public EnrichableException(String errorContext, String errorCode,
String errorMessage, Throwable cause){
super(cause);
addInfo(errorContext, errorCode, errorMessage);
}
public EnrichableException addInfo(
String errorContext, String errorCode, String errorText){
this.infoItems.add(
new InfoItem(errorContext, errorCode, errorText));
return this;
}
public String getCode(){
StringBuilder builder = new StringBuilder();
for(int i = this.infoItems.size()-1 ; i >=0; i--){
InfoItem info =
this.infoItems.get(i);
builder.append('[');
builder.append(info.errorContext);
builder.append(':');
builder.append(info.errorCode);
builder.append(']');
}
return builder.toString();
}
public String toString(){
StringBuilder builder = new StringBuilder();
builder.append(getCode());
builder.append('\n');
//append additional context information.
for(int i = this.infoItems.size()-1 ; i >=0; i--){
InfoItem info =
this.infoItems.get(i);
builder.append('[');
builder.append(info.errorContext);
builder.append(':');
builder.append(info.errorCode);
builder.append(']');
builder.append(info.errorText);
if(i>0) builder.append('\n');
}
//append root causes and text from this exception first.
if(getMessage() != null) {
builder.append('\n');
if(getCause() == null){
builder.append(getMessage());
} else if(!getMessage().equals(getCause().toString())){
builder.append(getMessage());
}
}
appendException(builder, getCause());
return builder.toString();
}
private void appendException(
StringBuilder builder, Throwable throwable){
if(throwable == null) return;
appendException(builder, throwable.getCause());
builder.append(throwable.toString());
builder.append('\n');
}
[L1:E1][L2:E2][L3:E3]
[L1:E1]Error in level 1, calling level 2
[L2:E2]Error in level 2, calling level 3
[L3:E3]Error at level 3
java.lang.IllegalArgumentException: incorrect argument passed
at exception.ExceptionTest$1.handle(ExceptionTest.java:8)
at exception.ExceptionTest.level3(ExceptionTest.java:49)
at exception.ExceptionTest.level2(ExceptionTest.java:38)
at exception.ExceptionTest.level1(ExceptionTest.java:29)
at exception.ExceptionTest.main(ExceptionTest.java:21)
Caused by: java.lang.IllegalArgumentException: incorrect argument passed
at exception.ExceptionTest.level4(ExceptionTest.java:54)
at exception.ExceptionTest.level3(ExceptionTest.java:47)
... 3 more
public class ExceptionTest {
protected ExceptionHandler exceptionHandler = new ExceptionHandler(){
public void handle(String errorContext, String errorCode,
String errorText, Throwable t){
if(! (t instanceof EnrichableException)){
throw new EnrichableException(
errorContext, errorCode, errorText, t);
} else {
((EnrichableException) t).addInfo(
errorContext, errorCode, errorText);
}
}
public void raise(String errorContext, String errorCode,
String errorText){
throw new EnrichableException(
errorContext, errorCode, errorText);
}
};
public static void main(String[] args){
ExceptionTest test = new ExceptionTest();
try{
test.level1();
} catch(Exception e){
e.printStackTrace();
}
}
public void level1(){
try{
level2();
} catch (EnrichableException e){
this.exceptionHandler.handle(
"L1", "E1", "Error in level 1, calling level 2", e);
throw e;
}
}
public void level2(){
try{
level3();
} catch (EnrichableException e){
this.exceptionHandler.handle(
"L2", "E2", "Error in level 2, calling level 3", e);
throw e;
}
}
public void level3(){
try{
level4();
} catch(Exception e){
this.exceptionHandler.handle(
"L3", "E3", "Error at level 3", e);
}
}
public void level4(){
throw new IllegalArgumentException("incorrect argument passed");
}
}
@reference_1_tutorials.jenkov.com
Exception Enrichment in Java