Interesting tidbits on ColdFusion Exceptions

This post is more than 2 years old.

A reader posted an interesting comment to my ColdFusion Exception Handling Guide. He had modified his error handling to store the errors in a database. This allowed him to look at history exception information, do trending, etc. But he ran into trouble trying to remove the stack trace from the exception object. Here is an example of that.

Imagine a simple onError like so:

<cffunction name="onError" returnType="void" output="false"> <cfargument name="exception" required="true"> <cfargument name="eventname" type="string" required="true"> <cfset structDelete(arguments.exception, "stacktrace")> <cfdump var="#exception#"> <cfabort> </cffunction>

Doing this causes the error handler itself to barf out: cannot remove properties from a java object. While I knew that Exceptions were Java objects, I had always assumed that when ColdFusion got to it, it was a normal struct. When you cfdump it, you see a struct, which is very different from the normal Java object dump. However, you can see that it is not if you check the class name:

<cfdump var="#exception.getClass().getName()#">

This returns coldfusion.runtime.UndefinedVariableException whereas a "real" structure would return coldfusion.runtime.Struct. Ok, so this implies that cfdump recognizes the ColdFusion exception and massages the output a bit. What happens if we try to duplicate the structure?

<cfset var ex = duplicate(arguments.exception)> <cfset structDelete(ex, "stacktrace")> <cfdump var="#ex#">

Unfortunately this returns the exact same error: cannot remove properties from a java object. So we still have a Java object after the duplicate. No surprise there I guess, but if cfdump had a 'hack' for ColdFusion exceptions I thought perhaps duplicate might.

I then tried this variation:

<cfset var newEx = structNew()> <cfloop item="key" collection="#arguments.exception#"> <cfset newEx[key] = duplicate(arguments.exception[key])> </cfloop>

<cfdump var="#newEx#"> <cfdump var="#newex.getClass().getName()#">

And bam - that did it. So at the key level the values came over correctly. And just to be sure, I then did this:

<cfset newEx.stackTrace = left(newEx.stackTrace, 100)>

And bam - that worked perfectly.

Of course, this may be overkill. If you are inserting the values from the exception object into the database, you can simply do the left in your cfquery. So for example, this is fine:

<cfoutput>#left(arguments.exception.stacktrace,10)#</cfoutput>

I'm not modifying the actual Exception object, just the result of getting the string value.

Raymond Camden's Picture

About Raymond Camden

Raymond is a senior developer evangelist for Adobe. He focuses on document services, JavaScript, and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can even buy me a coffee!

Lafayette, LA https://www.raymondcamden.com

Archived Comments

Comment 1 by Paul Dynan posted on 9/17/2009 at 6:52 PM

Brilliant, and worked pefectly. Thank you.

I did notice a caveat: The structs inside of Error remain as java objects (discovered while tryign to delete Error.RootCause.StackTrace).

I'm assuming I could just not add RootCause on the original copy, and loop it in using your method as an update after to further trim. Or, when I hit that key during the original copy, do an inner loop to build the contents of the key there.

I all probably seems excessive, but on just a query error, there four stacks and makes the error considerably longer than it needs to be. I feel it's akin to clearing a stack of papers off your desk, rather than just working around it all day.

Comment 2 by Raymond Camden posted on 9/17/2009 at 7:25 PM

Ah - so that means the sub parts of the exception obs are also complex. Nice. But yea, you could not copy the root cause, or make the copy code a bit more complex.

Comment 3 by Paul Dynan posted on 9/17/2009 at 7:40 PM

This seems to do the trick, drilling down enough to clean up most error dumps. I won't doubt there may be a simplier way yet to do the same thing (hoping the format comes out ok):
<CFSET newError = structNew()>
<CFLOOP ITEM="key" COLLECTION="#Error#">
<CFIF key IS NOT "StackTrace">
<cfset newError[key] = duplicate(Error[key])>
</CFIF>
<CFIF key IS "RootCause">
<CFSET RootCause = structNew()>
<CFLOOP ITEM="key1" COLLECTION="#Error.RootCause#">
<CFIF ListFind("Cause,RootCause", key1) GT 0>
<CFSET temp = structNew()>
<CFLOOP ITEM="key2" COLLECTION="#Error.RootCause["#key1#"]#">
<CFIF key2 IS NOT "StackTrace">
<CFSET temp[key2] = duplicate(Error.RootCause["#key1#"][key2])>
</CFIF>
</CFLOOP>
<CFSET RootCause[key1] = temp>
<CFELSEIF key1 IS NOT "StackTrace">
<CFSET RootCause[key1] = duplicate(Error.RootCause[key1])>
</CFIF>
</CFLOOP>
<CFSET newError[key] = RootCause>
</CFIF>
</CFLOOP>

Comment 4 by todd sharp posted on 9/17/2009 at 7:41 PM

Wonder if you can use serializeJSON() on the object? Not that that would be terribly useful, I'm just curious.

Comment 5 by Raymond Camden posted on 9/17/2009 at 7:44 PM

Todd! YOU FREAKING RULE! :) SerializeJSON does indeed work. :)

Comment 6 by Raymond Camden posted on 9/17/2009 at 7:45 PM

And even better, get this:

<cfset var s = serializeJSON(arguments.exception)>
<cfset var newEx = deserializeJSON(s)>

This gives you a vanilla struct. So you can manipulate as you will.

Comment 7 by Paul Dynan posted on 9/17/2009 at 8:17 PM

I still need my original code for our CF7 server, but this suggestion is working great on our CF8 server. Here's the improved version now:

<CFSET tempError = serializeJSON(Error)>
<CFSET newError = deserializeJSON(tempError)>

<CFLOOP LIST="newError,newError.RootCause,newError.RootCause.Cause,newError.RootCause.RootCause" INDEX="i">
<CFIF StructKeyExists( Evaluate(i), "StackTrace" )>
<CFSET temp = StructDelete( Evaluate(i), "StackTrace")>
</CFIF>
</CFLOOP>

Comment 8 by Alexandre Potvin Latreille posted on 3/8/2012 at 2:46 AM

You can never have enough information when logging errors, so that's what I do:

<cftry>
<cfthrow object="#arguments.exception#">
<cfcatch type="any">
<cfset var jsonData = serializeJSON({
'url' = url,
'form' = form,
'session' = session,
'cgi' = cgi,
'exception' = arguments.exception,
'application' = application,
'server' = server
})>
</cfcatch>
</cftry>

I can then save the whole thing in the database. This will serialize the stack trace as well.

Comment 9 by Alexandre Potvin Latreille posted on 3/8/2012 at 2:48 AM

There's a mistake in my previous post.

'exception' = arguments.exception -> 'exception' = cfcatch

Comment 10 by Raymond Camden posted on 3/8/2012 at 4:32 AM

Cool technique. Thanks for sharing that Alex.

Comment 11 by Alexandre Potvin Latreille posted on 3/8/2012 at 7:31 PM

There's at least one thing you need to watch with this solution: serializeJSON will crash with an uncatchable error if you pass it a struct that has a key which value is a reference to a function. This seems to be problematic for structs only, not CFCs. CFC instances are just serialized as empty structs by the serializeJSON function.

An obvious solution to this problem might be to make a copy of the structs, loop over their keys to remove any non-serializable members and then call serializeJSON over them.

Comment 12 by Alexandre Potvin Latreille posted on 3/8/2012 at 9:34 PM

I created a function that will create a serializable copy of an array or a struct.

<cffunction name="getSerializableObjectCopy" access="private" returntype="any">
<cfargument name="o" type="any" required="yes">

<cfscript>
var objectCopy = isStruct(arguments.o)? {} : [];
var sLen = isStruct(arguments.o)? structCount(arguments.o) : arrayLen(arguments.o);

if (isStruct(arguments.o)) {
var keyList = structKeyList(arguments.o);
}

for (var i = 1; i lte sLen; i++) {
local.keyOrIndex = isStruct(arguments.o)? listGetAt(keyList, i) : i;
local.value = arguments.o[local.keyOrIndex];

if (isStruct(local.value)) {
local.meta = getMetaData(local.value);
if (structKeyExists(local.meta, 'type') and local.meta.type is 'component') {
objectCopy[local.keyOrIndex] = '<#local.meta.fullName#>';
continue;
}
}

objectCopy[local.keyOrIndex] = (isStruct(local.value) or isArray(local.value))?
getSerializableObjectCopy(local.value, objectCopy) : (
isStruct(local.value)
or isSimpleValue(local.value)
or isArray(local.value)
)? local.value : '<unserializable value>';
}

return objectCopy;
</cfscript>
</cffunction>

Comment 13 by Alexandre Potvin Latreille posted on 3/8/2012 at 10:23 PM

I am sorry, the previous function is bugged. Can you delete my previous post. Thanks!

<cffunction name="getSerializableObjectCopy" access="private" output="yes" returntype="any">
<cfargument name="o" type="any" required="yes">

<cfscript>
if (isStruct(arguments.o)) {
local.objectCopy = {};
local.keys = structKeyArray(arguments.o);
local.sLen = arrayLen(local.keys);
} else {
local.objectCopy = [];
local.sLen = arrayLen(arguments.o);
}

for (var i = 1; i lte local.sLen; i++) {
local.keyOrIndex = isStruct(arguments.o)? local.keys[i] : i;
local.value = arguments.o[local.keyOrIndex];

if (isStruct(local.value)) {
local.meta = getMetaData(local.value);
if (structKeyExists(local.meta, 'type') and local.meta.type is 'component') {
local.objectCopy[local.keyOrIndex] = '<#local.meta.fullName#>';
continue;
}
}

local.objectCopy[local.keyOrIndex] = (isStruct(local.value) or isArray(local.value))?
getSerializableObjectCopy(local.value, local.objectCopy) : (
isStruct(local.value)
or isSimpleValue(local.value)
or isArray(local.value)
)? local.value : '<unserializable value>';
}

return local.objectCopy;
</cfscript>
</cffunction>

Comment 14 by Andrew posted on 7/7/2014 at 8:30 PM

I realize this post is pretty old, but I wanted to say thank you for both it and everyone's input.

I was trying to find a good way to serialize an exception object for storage in our error logging system and this helped me get it done.

At first I was trying to just use the SerializeJSON() function on the exception object, but I wanted to cut out a few parts of it before storage. So I tried serializing and then de-serializing it to a struct, but I kept seeing parts of the object get lost in translation and I couldn't control what it was doing.

Then I tried using the code Alexandre Potvin Latreille provided but found it did not work (in CF 10 at least).

So I took his code and re-wrote it a bit to account for types other than arrays, structs, and simple values.

It's located at http://pastebin.com/2XynXVKG if anyone is interested. So far it's worked very well for me.

Thanks again!