Experimenting With ExtendedInfo To Aggregate Error Information In ColdFusion
For the last few weeks, I've had the article, Java exception handling best practices by Lokesh Gupta, open in one of my browser tabs. While I don't program in Java, I think the article has a number of really thought-provoking points about proper error handling in any kind of application, regardless of language. The suggestions that stick out to me are:
- 6) Either log the exception or throw it but never do the both.
- 7) Never throw any exception from finally block.
- 8) Always catch only those exceptions that you can actually handle.
- 10) Use finally blocks instead of catch blocks if you are not going to handle exception.
- 11) Remember "Throw early catch late" principle.
- 13) Throw only relevant exception from a method.
- 17) Pass all relevant information to exceptions to make them informative as much as possible.
While some of these are about workflow, one theme that occurs to me is: more data equals easier debugging. In JavaScript, when you create an Error object and throw it, the error object can include any kind of data that you want. But, in ColdFusion, we're a bit more limited. Unless you start creating custom Java Exception objects, we have access to the throw() operator, which only allows for a few string-based fields.
Thinking back to my experiment with "Russian Doll" error reporting in Node.js, however, I wondered if the same sort of approach could be used in ColdFusion, albeit with a little more elbow-grease. In ColdFusion, the throw() operator accepts an extendedInfo property. This property has to be a string; but, in ColdFusion, we can easily serialize complex objects into strings. So, I wanted to see what it would look like to try and use the extendedInfo property to nest errors as you move up the call-stack.
The following demo is completely trite; so, try not to get distracted by the demo itself, but rather on the mechanics involved. I have a method that throws an error. The calling context then catches that error, wraps it, and throws another error, this time including any additional context information that may be relevant for debugging:
<cfscript>
try {
doMaths( "divide", 3, 0 );
} catch ( any error ) {
dumpError( error );
}
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// I execute the given binary operation on the given operands.
public numeric function doMaths(
required string operation,
required numeric operandOne,
required numeric operandTwo
) {
try {
if ( operation == "divide" ) {
return( divide( operandOne, operandTwo ) );
}
// ... other things, not necessary for demo.
} catch ( any error ) {
// If we get an error, we want to add additional relevant information
// so that when it moves up to the next level (and is eventually logged),
// we'll be more likely to have the complete picture for debugging. So that
// we don't lose the original error, I'm adding it to the current error as
// the extended information.
throw(
type = "MathsFailure",
message = "Math operation failed.",
detail = "Math operation [#operation#] failed with operands [#operandOne#], [#operandTwo#].",
extendedInfo = serializeJson( duplicate( error ) )
);
}
}
// I divide the first operand by the second operand.
public numeric function divide(
required numeric operandOne,
required numeric operandTwo
) {
if ( ! operandTwo ) {
throw(
type = "InvalidArgument",
message = "Divisor cannot be zero.",
detail = "You cannot divide [#operandOne#] by [#operandTwo#].",
extendedInfo = serializeJson( arguments )
);
}
return( operandOne / operandTwo );
}
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// I writeDump() the given error, after deserializing the nested extendedInfo error
// properties on the related errors.
public void function dumpError( required any error ) {
// We can't directly alter the top-level keys of an error object. As such,
// we need to coerce it into a ColdFusion struct so that we can alter the
// extendedInfo key if it contains JSON.
var outerError = errorToStruct( error );
var currentError = outerError;
// Keep traversing the error object tree while the extendedInfo exists and
// and represents JSON data.
while (
structKeyExists( currentError, "extendedInfo" ) &&
isJson( currentError.extendedInfo )
) {
currentError.extendedInfo = deserializeJson( currentError.extendedInfo );
// Travel down the error chain.
currentError = currentError.extendedInfo;
}
// Now that we've traveled down the object tree, dump out the top-level
// error object.
writeDump( outerError );
}
// I coerce the given ColdFusion error object into a ColdFusion struct.
public struct function errorToStruct( required any error ) {
var errorAsData = {};
structAppend( errorAsData, error );
return( errorAsData );
}
</cfscript>
As you can see, as the error moves up the call-stack, it is caught, serialized as JSON (JavaScript Object Notation), and added to a new error as the "extendedInfo" property. The new error, therefore, contains the root error as well as any other information that might be relevant for debugging.
When the error finally bubbles up to the top-level exception handler, it is dumped out. For the purposes of easy consumption, my dump operation traverses the error object and deserializes the extendedInfo fields so that they can be easily read when output:
As you can see, the top-level error object becomes a "Russian doll", so to speak, of error data as the original throw travels up the call-stack.
Even if you think this idea is crazy, maybe you can still see some value in using the "extendedInfo" property to capture additional data about an error context. I have found this particularly helpful when dealing with external APIs that communicate over HTTP. Imagine that you have to communicate with an HTTP API and it returns a 500 status code. Typically, in that situation, I'll throw a UnexpectedError error; but, I'll often use the extendedInfo property to include information about why the HTTP request failed (like its response body). This has been super helpful for debugging.
The more I think about the above list of error handling practices, the more I think about the importance of this one:
Either log the exception or throw it but never do the both.
Being able to nest error objects makes this possible while, at the same time, not losing fidelity of information. Anyway, just some noodling on error handling in ColdFusion. Think of this as an experiment and not necessarily a suggestion.
Want to use code from this post? Check out the license.
Reader Comments
@Ben:
The one problem with using serializeJSON() is that serialization bugs could result in the "catch" being transformed in some ways that might end up being misleading when debugging. It might be better to serialize the catch using ByteArrayOutputStream, then store it as Base64 (see https://www.petefreitag.com/item/649.cfm for an example).
This should guarantee that the object isn't altered in any subtle ways that could lead to confusion.
@Ben, @Dan G. Switzer, II
My first impression of this is... yes! I'm definitely going to play with this approach and have a couple places in mind straight off that I think this will be helpful in my codebase. I also appreciate Dan chiming in with his suggestion to maintain the integrity of the data structure. I'd never thought to convert to a byteArray then b64 the stream. Also worthy of experimentation. You two have inspired me today, and I appreciate that! Thanks!
@Dan,
Really interesting. I don't think that I knew that was even possible. I know that there are objectSave() and objectLoad() methods in ColdFusion - I wonder if they are using this API under the hood. I've experimented with them a tiny bit; but, never really found a use-case for them in my limited experience.
That said, since this is for debugging purposes, the 100% fidelity of the JSON serialization life-cycle might not be a deal breaker. Meaning, if something were to serialize "true" to be "YES" (for example), the subsequent log item would still be human-consumable.
Of course, how tired is everyone of the fact that JSON in ColdFusion doesn't just "work".... like it does is like every other language :(
@Chris,
Awesome :D Glad you're enjoying the conversation!