Ask Ben: Handling Errors With ColdFusion CFError

Posted August 29, 2007 at 8:23 AM

Tags: Ask Ben, ColdFusion

A tutorial on how to get ANY ColdFusion error emailed to you automatically and a friendly message displayed to the visitor would be great, I found one on EasyCFM.com but doesn't actually work.

Having looked at the EasyCFM tutorial, it looks like you want to know about how to use ColdFusion's CFError tag. Before I go into this, I have to just say that if you are using ColdFusion 7 or greater, I would recommend moving to the Application.cfc model and use the OnError() application event method handler; it just provides a nice, clean way of doing this. But that is a whole other discussion. For now, I will just cover the use of the ColdFusion CFError tag and how it can be used for error handling.

To start off with, we have to put the ColdFusion CFError tag on a template that will be executed for every page request such that every new page request will know how to properly handle errors. The obvious choice is to use the Application.cfm template:

 Launch code in new window » Download code as text file »

  • <!--- Kill extra output. --->
  • <cfsilent>
  •  
  • <!--- Define the application settings. --->
  • <cfapplication
  • name="ErrorHandlingDemo"
  • applicationtimeout="#CreateTimeSpan( 0, 0, 1, 0 )#"
  • sessionmanagement="true"
  • sessiontimeout="#CreateTimeSpan( 0, 0, 1, 0 )#"
  • setclientcookies="true"
  • />
  •  
  • <!--- Define the page request settigs. --->
  • <cfsetting
  • requesttimeout="20"
  • showdebugoutput="false"
  • />
  •  
  • <!--- Define erorr handling template. --->
  • <cferror
  • type="exception"
  • template="./cferror.cfm"
  • />
  •  
  • <!--- Include functions. --->
  • <cfinclude template="functions.cfm" />
  •  
  • </cfsilent>

As you can see here, with the ColdFusion CFError tag we are telling it to handle "Exception" based errors with the template, "cferror.cfm". The template path that you want to use with this tag is relative to the Application.cfm file itself; it does not matter which page in the application actually throws the root error - like the ColdFusion CFInclude tag, the path is relative to the tag-defining template not the page template.

Also, if you look at the documentation, you will see that you can define more that one CFError tag - one for each of the possible error types. Don't worry about this. You pretty much will never do that. Stick to Exception. The others no longer serve any real purpose. In fact, if you are using the other types, you might want to reconsider what you are doing (god forbid you are using the built-in ColdFusion data validation!!!).

The template that we are pointing to, cferror.cfm, is not really special in any way. It's just a ColdFusion template like any other. The only real difference is that if the template gets included by the CFError tag, a new variable is created: VARIABLES.Error. This Error object contains the information about the exception that was thrown by the code. This will include things like the stack trace, tag context, message, detail, and template and line number where the error occurred.

Other than that, the only other thing you have to worry about is whether or not content has already been flushed to the browser. If the exception was thrown before the content buffer started flushing, then the CFError template has a blank slate to work with. If, however, content has already been flushed to the browser at the time of the error, then the CFError template will already have a partial page displayed. If one of the goals here is to show a nice "Error Page" to people, then this is something to be weary of. In the following page, you will see that we try to set some header values. If that fails (indicating that content has already gone to the client), we will redirect the user back to the error page explicitly so that we can start a new page. Of course, either way, we want to mail the error to someone.

Here it my example cferror.cfm ColdFusion template:

 Launch code in new window » Download code as text file »

  • <!--- Kill extra output. --->
  • <cfsilent>
  •  
  • <!---
  • Check to see if the error object exists. Even though
  • we have landed on this page, it is possible that
  • someone called it directly without throwing an erorr.
  • The error object only exists if an error was caught.
  • --->
  • <cfif StructKeyExists( VARIABLES, "Error" )>
  •  
  • <!---
  • Mail out the error data (and whatever other scopes
  • you would like to see at the time of th error). When
  • you CFDump out the objects, make them Secure AND
  • also be sure to use a TOP attribute when appropriate
  • so that the CFDump doesn't recurse forever.
  • --->
  • <cfmail
  • to="ben@xxxxxxxxxx.com"
  • from="web-errors@xxxxxxxxxx.com"
  • subject="Web Site Error"
  • type="html">
  •  
  • <p>
  • An error occurred at
  • #DateFormat( Now(), "mmm d, yyyy" )# at
  • #TimeFormat( Now(), "hh:mm TT" )#
  • </p>
  •  
  • <h3>
  • Error
  • </h3>
  •  
  • <cfdump
  • var="#MakeStructSecure( VARIABLES.Error )#"
  • label="Error object."
  • />
  •  
  • <h3>
  • CGI
  • </h3>
  •  
  • <cfdump
  • var="#MakeStructSecure( CGI )#"
  • label="CGI object"
  • />
  •  
  • <h3>
  • REQUEST
  • </h3>
  •  
  • <cfdump
  • var="#MakeStructSecure( REQUEST )#"
  • label="REQUEST object"
  • top="5"
  • />
  •  
  • <h3>
  • FORM
  • </h3>
  •  
  • <cfdump
  • var="#MakeStructSecure( FORM )#"
  • label="FORM object"
  • top="5"
  • />
  •  
  • <h3>
  • URL
  • </h3>
  •  
  • <cfdump
  • var="#MakeStructSecure( URL )#"
  • label="URL object"
  • top="5"
  • />
  •  
  • <h3>
  • SESSION
  • </h3>
  •  
  • <cfdump
  • var="#MakeStructSecure( SESSION )#"
  • label="SESSION object"
  • top="5"
  • />
  •  
  • </cfmail>
  •  
  • </cfif>
  •  
  •  
  •  
  • <!---
  • When setting the header information, be sure to put
  • it in a CFTry / CFCatch. We can only send header
  • information if the site has NOT already been flushed
  • to the browser. Also set a flag so that we know if
  • information has been committed.
  • --->
  • <cfset REQUEST.RequestCommitted = false />
  •  
  • <cftry>
  • <!--- Set the status code to internal server error. --->
  • <cfheader
  • statuscode="500"
  • statustext="Internal Server Error"
  • />
  •  
  • <!--- Set the content type. --->
  • <cfcontent
  • type="text/html"
  • reset="true"
  • />
  •  
  • <!--- Catch any errors. --->
  • <cfcatch>
  •  
  • <!---
  • There was an error so flag the request as
  • already being committed.
  • --->
  • <cfset REQUEST.RequestCommitted = true />
  •  
  • </cfcatch>
  • </cftry>
  •  
  • </cfsilent>
  •  
  • <!---
  • Check to see if the request has been committed. If it
  • has, then it means that content has already been committed
  • to the browser. In that case, we are gonna want to refresh
  • the screen, unless we came from a refresh, in which case
  • just let the page run.
  • --->
  • <cfif (
  • StructKeyExists( VARIABLES, "Error" ) AND
  • REQUEST.RequestCommitted AND
  • (NOT StructKeyExists( URL, "norefresh" ))
  • )>
  •  
  • <script type="text/javascript">
  •  
  • window.location.href = "cferror.cfm?norefresh=true";
  •  
  • </script>
  •  
  • <!--- Exit out of the template. --->
  • <cfexit />
  •  
  • </cfif>
  •  
  •  
  • <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  • <html>
  • <head>
  • <title>An Error Occurred</title>
  • </head>
  • <body>
  •  
  • <h1>
  • Internal Server Error
  • </h1>
  •  
  • <p>
  • An internal server error has occurred, but our
  • squad of CF Ninjas are looking into it! Naturally,
  • you won't be able to tell that they're doing
  • anything because they are Ninjas! But, rest assured,
  • stuff is getting done.
  • </p>
  •  
  • </body>
  • </html>

A few minor things to notice in the above template. First, we are checking to see if the variable Error exists in the VARIABLES scope. If it does, then we are sending out the error email. This will stop us from trying to reference the Error object if the cferror.cfm page was called directly. Also notice that when we CFDump out our relevant scopes, we have two things going on:

  1. We are using the TOP attribute.
  2. We are using the UDF, MakeStructSecure().

The TOP attribute of the ColdFusion CFDump tag is VERY awesome. It limits the depth of recursion when dumping out the contents of the given variables. It also limits query and array dumping, but that's not what's so cool at this moment. If you ever have a struct that has circular references (like a bi-directional linked list), your CFDump will possibly crash the server because it never knows when to stop. By telling the CFDump tag to stop at 5 levels, even if you have circular references, it will limit the recursive depth to 5 - wicked sweet!

The MakeStructSecure() ColdFusion user defined function is a method that I built based on a tip I got from Tobe Goldfinger of the New Your ColdFusion User group. She was saying that when we mail ourselves error information, often times we forget that it might contain secure information such as credit card numbers and expiration dates. Obviously, we don't want to be sending that information out over the unsecured email pathways, so this MakeStructSecure() recursively searches through the given struct looking for keys that look suspicious so that it can black out the values:

 Launch code in new window » Download code as text file »

  • <!---
  • Define function that will recursively search a
  • struct and it's nested elements for keys that should
  • be considered secure and blacked out.
  • --->
  • <cffunction
  • name="MakeStructSecure"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="Does a very cursory job of cleaning up a struct by blacking out secure information.">
  •  
  • <!--- Define argumets. --->
  • <cfargument
  • name="Struct"
  • type="struct"
  • required="true"
  • hint="The struct we are going to clean."
  • />
  •  
  • <cfargument
  • name="Depth"
  • type="numeric"
  • required="false"
  • default="1"
  • hint="The depth of the current search - this will stop the function from looping infinitely."
  • />
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = StructNew() />
  •  
  • <!---
  • Check to see if we have reached our max depth
  • for this search.
  • --->
  • <cfif (ARGUMENTS.Depth GTE 5)>
  •  
  • <!---
  • You're going too deep, it hurts! We might be
  • looping in a circular struct reference.
  • --->
  • <cfreturn />
  •  
  • </cfif>
  •  
  •  
  • <!---
  • ASSERT: If we have made it this far, then we have
  • to check all the struct keys at this level.
  • --->
  •  
  • <!---
  • Define the list of keys that would be considered
  • to hold secure data.
  • --->
  • <cfsavecontent variable="LOCAL.SecureKeys">
  • CreditCard
  • CCNumber
  • CCNum
  • ExpirationDate
  • Expry
  • ExpDate
  • CCExp
  • </cfsavecontent>
  •  
  •  
  • <!---
  • Loop over the struct looking for keys that would
  • flag secure data to be removed.
  • --->
  • <cfloop
  • item="LOCAL.Key"
  • collection="#ARGUMENTS.Struct#">
  •  
  • <!---
  • Check to see if the key is to be considered
  • secure AND that the value in it is simple.
  • --->
  • <cfif (
  • FindNoCase( LOCAL.Key, LOCAL.SecureKeys ) AND
  • IsSimpleValue( ARGUMENTS.Struct[ LOCAL.Key ] )
  • )>
  •  
  • <!--- Black out value. --->
  • <cfset ARGUMENTS.Struct[ LOCAL.Key ] = RepeatString(
  • "*",
  • Len( ARGUMENTS.Struct[ LOCAL.Key ] )
  • ) />
  •  
  • <!---
  • Check to see if this key is a struct that we
  • might have to search through.
  • --->
  • <cfelseif IsStruct( ARGUMENTS.Struct[ LOCAL.Key ] )>
  •  
  • <!---
  • Recurse through this one. Be sure to send
  • through a new depth value so we don't loop
  • forever.
  • --->
  • <cfset MakeStructSecure(
  • Struct = ARGUMENTS.Struct[ LOCAL.Key ],
  • Depth = (ARGUMENTS.Depth + 1)
  • ) />
  •  
  • </cfif>
  •  
  • </cfloop>
  •  
  •  
  • <!--- Return out. --->
  • <cfif (ARGUMENTS.Depth EQ 1)>
  •  
  • <!--- Return the cleaned struct. --->
  • <cfreturn ARGUMENTS.Struct />
  •  
  • <cfelse>
  • <cfreturn />
  • </cfif>
  • </cffunction>

Here, this ColdFusion user defined function is looking for the following keys:

  • CreditCard
  • CCNumber
  • CCNum
  • ExpirationDate
  • Expry
  • ExpDate
  • CCExp

Now, this can work, but if you KNOW that one of your structs (ex. the FORM scope) might have secure information, I would check for this explicitly. Do NOT rely on this function to actually work. I put it in more to have a little fun and to demonstrate that we need to be actively thinking about this kind of stuff.

Right before we display the error page, we try to set some header information using the CFHeader / CFContent tag. If this works, then we know that we have a blank page to work with. However, if this fails (you cannot set header information on a page response that has already been committed to the browser), it means that some of the page content has already been flushed. If that is the case, we are using Javascript to immediately forward the user (browser refresh) to the cferror.cfm page. At this point, the CFMail has already been sent out, we just care about proper display. Notice that when we refresh the browsr, we are passing along a flag so the cferror.cfm template knows not to do that again if asked to (this will prevent infinite forwarding).

Note: There are other ways to check to see if the request has already been committed, but I am trying to keep this a bit more low-level.

The last thing left is just to throw an error to see this in action :) In our index.cfm file, we are going to refer to an undefined variable:

 Launch code in new window » Download code as text file »

  • <!--- Kill extra output. --->
  • <cfsilent>
  •  
  • <!--- Set some sercur form data. --->
  • <cfset FORM.CreditCard = "1234567890123456" />
  • <cfset FORM.ExpirationDate = "12/12" />
  •  
  • </cfsilent>
  •  
  • <html>
  • <head>
  • <title>ColdFusion CFError Handling Tutorial</title>
  • </head>
  • <body>
  •  
  • <!--- Force an error. --->
  • <cfset asdf = sf />
  •  
  • </body>
  • </html>

Just to demonstrate that the MakeStructSecure() method is actually doing something, I have thrown some credit card information into the FORM scope. Anyway, throwing the error above provides the user friendly error page and sends out the email. The email that we get looks like this:


 
 
 

 
 ColdFusion CFError Email CFDump Data Display  
 
 
 

Notice that we get our nicely formatted HTML email and that the secure FORM data has been escaped.

Hope this helped a bit or at least pointed you in the right direction.

Download Code Snippet ZIP File

Post Comment  |  Ask Ben  |  Permalink  |  Other Searches  |  Print Page




Learning ColdFusion 9 - ColdFusion 9 tutorials, samples, examples, demos

Reader Comments

Rob Rawlins
Aug 29, 2007 at 10:10 AM // reply »
41 Comments

Great work Ben,

I implemented a similar concept into my app just a moment ago, using the onError() method from my application.cfc, I've already spotted and fixed 5 bugs that I wasnt aware of as they existed in an old web service which isnt used for user iteraction, so there was no one to report the error to me :-D

Great stuff mate, really great stuff, I'd been thinking about doing this for a long while, just needed a firm kick in the pants to get it done.

*thumbs up

Rob


Aug 29, 2007 at 10:22 AM // reply »
5,406 Comments

@Rob,

Thanks a lot. Also, since you mentioned OnError(), I just wanted to mention to other people that OnError() is great because it is a very clean way to integrate with the application level events, but at the same time, most of the same issues are still present (partial page flushing, potentially undefined error object).

Glad to see error handling is working out so nicely for you :)


Aug 29, 2007 at 10:28 AM // reply »
26 Comments

lots of great info in there, Ben. Thanks for the thorough explanations! How do you have time to get actual work done? Does your job mind you spending time blogging during work hours?

Anyways, here's a custom tag I worked up over the years, though I use onError() now in CF 7 and 8:

http://cfzen.instantspot.com/blog/index.cfm/2007/5/4/heres-my-errorHandlercfm-what-do-you-think

(it's open for comment)

cheers.


Aug 29, 2007 at 11:23 AM // reply »
25 Comments

Some good ideas in here Ben! You might want to also consider removing the CFID/CFTOKEN since theoretically someone could use those to hijack another user's session, and possibly view secure information, depending on your application. With my errors, I took the approach of writing the full error dumps out to a protected .cfm file and just email the admin a summary of the error. The full error pages are viewable through an admin login area and SSL.


michael white
Aug 29, 2007 at 12:06 PM // reply »
17 Comments

now if the error was actually a database error and you wanted to see the sql, you could just add a cfif section for the error type?


Dustin
Aug 29, 2007 at 1:02 PM // reply »
42 Comments

@Michael

If you are catching db errors you'll output these:

<cfif IsDefined("catch.SQLState")>
SQL State: #catch.SQLState#
</cfif>
<cfif IsDefined("catch.Sql")>
SQL: #catch.Sql#
</cfif>
<cfif IsDefined("catch.queryError")>
Query Error: #catch.queryError#
</cfif>
<cfif IsDefined("catch.where")>
Where :#catch.where#
</cfif>


Aug 29, 2007 at 3:50 PM // reply »
25 Comments

Has anyone found a way to dump the local var scope in a CFC? It doesn't seem that ColdFusion puts them into any named scope which makes it really frustrating to track down an intermittent error in a CFC.


Sep 9, 2007 at 6:07 PM // reply »
5,406 Comments

@Mary Jo,

I can't seem to find the link right now, but I recently read a blog post about someone who got the "Active Local Scope" from the Page Context object.

GetPageContext().getActiveFunctionLocalScope()

Give that a go.


Sep 9, 2007 at 10:08 PM // reply »
25 Comments

Thanks Ben, looks like you blogged about all the GetPageContext stuff awhile back. ;-)

http://www.bennadel.com/blog/758-ColdFusion-GetPageContext-Massive-Exploration.htm

It does seem that the local scope still gets destroyed before going to a site-wide error handler (which I guess you would expect but what I'd really like to find a way around), but at least this will give me a tool when I need to remotely debug something in a CFC.


Post Comment  |  Ask Ben

Recent Blog Comments
Secret Admirer
Jul 4, 2009 at 12:23 PM
Project HUGE: Huge In A Hurry - Get Big - Phase 2 / Week 3
My Poor Dreamboat :( I feel so sad when I know you are hurting. I hope you feel better soon. ... read »
Jul 4, 2009 at 9:42 AM
FLV 404 Error On Windows 2003 Server
I bookmarked this page. Thanks for given this great post.... ... read »
Jul 4, 2009 at 4:00 AM
Terms Of Service / Privacy Policy Document Generator
thanks ben, I'm not a big fan of contracts so to find your no no-nesense ToS generator has helped me no end. all the best matt ... read »
Justice
Jul 3, 2009 at 11:10 PM
Create A Running Average Without Storing Individual Values
@Ben, I think you're going about this the wrong way. You're trying to use complicated techniques when there is a simple and beautiful technique readily available (a la Gary Funk's comment). Instead ... read »
Bob
Jul 3, 2009 at 9:19 PM
Project HUGE: Huge In A Hurry - Get Big - Phase 3 / Week 1
a good technical explanation http://crossfitphoenix.typepad.com/crossfit_phoenix_forging_/the-overhead-squat.html ... read »
Jul 3, 2009 at 9:03 PM
Create A Running Average Without Storing Individual Values
If I wanted to do this and only carry two numbers, I'd keep track of the sum and N. Then you are pretty much accurate all the time. average = (sum + new_number) / (N + 1) But all this was in a for ... read »
Roland Collins
Jul 3, 2009 at 8:58 PM
Create A Running Average Without Storing Individual Values
@Martin - not just floating point though. Depending on what langauge you're working in, decimals can cause just as many headaches if they're not precise enough. But again, for most applications, th ... read »
Isnogood
Jul 3, 2009 at 7:16 PM
Project HUGE: Huge In A Hurry - Get Big - Phase 3 / Week 1
Watch this http://www.nsca-lift.org/videos/default.shtml ... read »