The ErrorInfo List
Jakob Jenkov |
As the AppException
is propagated up the call stack, a list of ErrorInfo
objects
are built up inside it. In this text I will examine that list, and how to extract information from it.
The ErrorInfo
list can contain more than one ErrorInfo
object. So, how do you determine
what type of error the AppException
represents? What is the severity? What error message should you show
to the user? Or write to the application log? I mean, given that you could have multiple severities and error types
embedded in the different ErrorInfo
objects, how do you determine the real type and severity of the error?
A ConfigFileLoader Example
Here is an example ErrorInfo
list for an exception thrown during the loading of an important
configuration file:
errorId | contextId | type | severity | cause |
FileLoadError | FileLoader | client | error | FileNotFoundException |
ConfigFileLoadError | ConfigFileLoader | internal | fatal |
This ErrorInfo
list originates from an AppException
being thrown by a
FileLoader
component. This component only knows that the file loading error, is
of type client
(wrong path given), and of severity error
.
The FileLoader
component was called by a ComfigFileLoader
component. The ConfigFileLoader
component knows that if configuration file is not loaded
correctly, the whole application will not work correctly. Therefore the ConfigFileLoader
adds that information to the ErrorInfo
list. It sets the error type to internal
,
since either the configuration file is missing, or the paths to the configuration file is wrong. This is
an internal error. Additionally, the ConfigFileLoader
sets the severity
to fatal
, since the application
cannot run correctly without the configuration file.
So, which of the two ErrorInfo
objects should we use, when notifying users and non-users?
The answer is: A combination of them all.
In the example above, it is the second ErrorInfo
object (ConfigFileLoader
) that contains most correct information
about the severity and type of the error. However, the first ErrorInfo
object contains more information about what failed (FileNotFoundException
).
So, when it comes to severity
and type
, use the latest ErrorInfo
object.
When it comes to logging diagnostic information, use all the ErrorInfo
objects.
A UserFileLoader Example
Here, I will look at a similar example to the one mentioned above:
errorId | contextId | type | severity | cause |
FileLoadError | FileLoader | client | error | FileNotFoundException |
UserFileLoadError | UserFileLoader | client | warning |
The FileLoader
component is called by the UserFileLoader
, which loads files which
the user of the application provides the paths for. Thus, if an error occurrs during the loading of that file,
it is not an error in the application. It is most likely just the user providing an invalid file path, or
the file pointed to has invalid content. As soon as the user points the application to an existing, valid file,
the error will go away.
A StreamLoader Example
Imagine that the fileLoader()
method calls a streamLoader()
method underneath.
The streamLoader()
method is given a stream to load the file from.
The streamLoader()
method does not know where the stream is connected to, but the
calling method most likely does. If the stream loading fails, here is an example of how the
ErrorInfo
list could look:
errorId | contextId | type | severity | cause | parameters |
StreamLoadError | StreamLoader | client | error | ||
FileLoadError | FileLoader |
Notice how the second ErrorInfo
object does not update the severity
field. It has no
new information about the severity, so it leaves it set to whatever the first ErrorInfo
object set it
to.
Of course, in a real application, the fileLoader()
method would probably be called from some other
method, which may actually know more about the severity of the error. Like this:
errorId | contextId | type | severity | cause | parameters |
StreamLoadError | StreamLoader | client | error | ||
FileLoadError | FileLoader | ||||
UserFileLoadError | UserFileLoader | warning |
Notice, however, how the last ErrorInfo
object does not know anything new about the
errorType
, so it leaves it untouched.
Extracting Severity and Error Type from the ErrorInfo List
The severity and error type of a given AppException
is used to determine how to handle
the exception, and which group of relevant parties to notify. Since the ErrorInfo
list can contain multiple severity and error type codes, you need to somehow filter them down to
one each. Otherwise, how do you know what to do about a given exception? I mean, if it is both
listed as a warning and an error, and as a service and client error?
If you follow the rule of only setting the error type and severity if you actually know something new about it, you should use the latest severity and error type. Here is a code example:
int errorType = -1; for(ErrorInfo errorInfo : e.getErrorInfoList()){ if(errorInfo.getErrorType() != -1) { errorType = errorInfo.getErrorType(); } } int severity = -1; for(ErrorInfo errorInfo : e.getErrorInfoList()){ if(errorInfo.getSeverity() != -1) { severity = errorInfo.getSeverity(); } }
After the above two for-loops have executed, the errorType
and severity
variables
will contain the latest set value of error type and severity.
You may decide to encapsulate the two loops in each their method (I probably would).
You probably won't be able to measure the performance difference, since exceptions do not happen
that often, and since the ErrorInfo
list is probably not going to be that deep.
Extracting Error ID from the ErrorInfo List
Extracting the error ID and error descriptions from the ErrorInfo
object list is a bit
different than extracting error type and severity.
For error type and severity you are only interested in one value. For error ID and error descriptions
you are interested in all the values. The complete error id is thus composed of all the error id's and
context id's in ErrorInfo
list.
Here is a code example method that illustrates how to extract the total error ID:
public static String extractErrorId(AppException e){ StringBuilder builder = new StringBuilder(); for(int i=e.getErrorInfoList().size()-1; i>=0; i--){ ErrorInfo errorInfo = e.getErrorInfoList().get(i); builder.append(errorInfo.getContextId()); builder.append(":"); builder.append(errorInfo.getErrorId()); if(i>0){ builder.append("/"); } } return builder.toString(); }
This method creates a total error ID which is made up of all the context ID's and error ID's of the
ErrorInfo
list.
Notice that the list is iterated backwards, from last to first ErrorInfo
object. Remember,
the ErrorInfo
objects are added to the AppException
on the way up the call stack.
The path to the exception in context ID's and error ID's is thus bottom up in the list. By iterating
the list in reverse order, the path is changed to top down, reflecting the context path from the top
of the application down to the location of the error.
Here is an example total error ID:
UserFileLoader:FileLoadError/FileLoader:FileLoadError
Extracting User Error Descriptions
The user should be given a simple error message when something fails in your application. If the error is caused by the users behaviour, and thus can be corrected by the user, tell the user what the error was, and how to correct it.
If, on the other hand, the error is caused by something which is out of the users hands, don't give the user too much detail. Just let the user know that the requested action failed, and that it has been logged and will be investigated. You can use pretty much the same user error message for all these types of errors, as giving the user internal error ID's, parameter information etc. is not going to help the user. On the contrary, you risk giving away information that can be used by hackers to break into your application.
Error Type Determines User Error Descriptions
You look at the errorType
to determine if the error was caused by the user, or
something else. If the error type is "client", that means that the client (the user) has used
the application in a wrong way. By correcting the usage, the error will go away. In this case,
tell the user what went wrong, and tell them how to correct their error.
If the errorType
is either service
or internal
, then you
all you do is to show the user a standard error message, saying that the error has been logged,
and will be investigated.
Extracting Non-User Error Descriptions
The non-user error description is used by the application operators and developers to determine
the cause of the error, and how to correct it. Therefore, it should be as detailed as possible.
The non-user error description should therefore include all the ErrorInfo
objects
details, and not just one of ErrorInfo
objects.
The full error description becomes quite large, and possibly messy and unstructured. Therefore, it might make sense to convert it to an XML structure, and log that full structure. Having an XML structure for the error details may also make it easier for tools to monitor the log, and extract the bits of information out of it, they need.
Here is an example XML structure for an error report:
<error> <fullErrorId> UserFileLoader:LoadError/FileLoader:FileLoadError </fullErrorId> <errorType>client</errorType> <severity>warning</severity> <errorInfoList> <errorInfo> <contextId>FileLoader</contextId> <errorId>FileLoadError</errorId> <errorType>client</errorType> <severity>error</severity> <errorDescription> The file was found, but could not be parsed correctly. The following error was found in the file: ... </errorDescription> <parameters> <parameter name="file">c:\data\myFile.txt</parameter> </parameters> </errorInfo> <errorInfo> <contextId>UserFileLoader</contextId> <errorId>FileLoadError</errorId> <errorType>client</errorType> <severity>warning</severity> <errorDescription> An error occurred during the processing of a file which the user requested the application to process. </errorDescription> <errorCorrection> Point to a file that actually exists. </errorCorrection> <parameters> <parameter name="file">c:\data\myFile.txt</parameter> </parameters> </errorInfo> </errorInfoList> </error>
Tweet | |
Jakob Jenkov |