Skip to main content
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Alec Irwin
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Alec Irwin

Experimenting With RESTful Error Response Codes And CFThrow's ErrorCode Attribute

By on
Tags:

Earlier this week, in my comments about understanding MVC (Model-View-Controller) architecture in ColdFusion, I was describing my API workflow to Steven Neiland. When processing a RESTful request, I tend to use a CFTry-CFCatch-CFThrow workflow that short-circuits request processing using explicit exceptions. This workflow prevents the request from doing any extraneous processing by failing fast. As I was describing the workflow, however, I realized that my approach doesn't give the 3rd party API consumers much information to work with; since I have to squeeze a large number errors into a small subset of HTTP Status Codes, the cause of the error becomes a bit ambiguous. As such, I wanted to look into providing more error-specific information.

Before I started playing with code, I did some Google searches to see what other big API providers were doing. By far, the best example I found was on the Twilio website. Twilio maintains a long list of error codes that [seemingly] covers the entire set of possible errors that their API might return.

These error codes give the 3rd party API consumer-developer a lot of insight into what exactly is going wrong with the request. It's one thing to get a "400" response; it's another thing to get a "400" response because the "voice" attribute of the "Say" XML tag is invalid. With that kind of information, not only is debugging easier; but, it also becomes much easier to provide a user-friendly error message to any users of the 3rd party API client.

In order to facilitate this approach, we need a central list of possible errors as well as a way to map exceptions onto error codes. Luckily, ColdFusion's CFThrow tag has an "errorCode" attribute for this very purpose. I've never used it before, but the errorCode attribute appears to accept any simple value. This value can then be retrieved from the exception object within the context of a CFCatch tag.

As far as organizing the list of possible error codes, I didn't know what the best move would be; so, for the following demo, I'm just inlining it as a ColdFusion struct within the API processing workflow.

The following demo / exploration processes a single, implicit resources that expects a Name and Age attribute. If either of these values are missing or are invalid, individual exceptions will be raised.

Index.cfm - Our Primary API Entry Point

<!---
	For this demo, we are only going to deal with a single
	"resource" that accepts NAME and AGE values. We don't want to
	param them here so that we can return more granular data in
	the error response.

	param url.name
	param url.age
--->


<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->


<!---
	Set up some default values in the request. This data will help
	us determine what kind of response we ultimately want to return
	to the client.
--->

<!---
	The status code will mimic the HTTP status code (in case the
	status code is being suppressed for clients that can't handle
	non-200 responses.
--->
<cfset request.statusCode = 200 />
<cfset request.statusText = "OK" />

<!---
	The Response key is the resource payload being returned from the
	API. This is the ONLY KEY that will be OVERRIDDEN directly by the
	resource handlers. Every other key will be set by the primary
	RESTful controller.
--->
<cfset request.response = true />

<!---
	This is the error code and message for any errors that occurred
	during the API request.
--->
<cfset request.errorCode = 0 />
<cfset request.errorText = "" />
<cfset request.errorDetail = "" />


<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->


<!---
	Because there are only a small number (relavitely speaking) of
	generic HTTP status codes, we want to be able to provide more
	insightful error codes that are relevant to our RESTful API. Each
	error response should include one of the following values that
	the 3rd party API developer can use when interacting with and
	debugging their API responses.
--->
<cfset request.errorCodes = {} />
<cfset request.errorCodes[ "10000" ] = "Unexpected error." />
<cfset request.errorCodes[ "11011" ] = "Name is missing." />
<cfset request.errorCodes[ "11012" ] = "Name is invalid." />
<cfset request.errorCodes[ "11021" ] = "Age is missing." />
<cfset request.errorCodes[ "11022" ] = "Age is invalid." />


<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->


<!---
	While processing the API request, we any number of things could
	go wrong - both expected and unexpected. As such, we'll wrap all
	processing in a Try/Catch block so that we can intercept errors
	and turn them into appropriate RESTful error response values.

	NOTE: Custom errors (via CFThrow) must include an [errorCode]
	attribute so that the error responses can be constructed properly.
--->
<cftry>

	<!---
		Include the resource processing controller. Imagine that
		this routes to one of many possible resources (even though
		this demo only has ONE resource to process).
	--->
	<!--- ------------------------------------------------- --->
	<!--- ------------------------------------------------- --->
	<cfinclude template="process.cfm" />
	<!--- ------------------------------------------------- --->
	<!--- ------------------------------------------------- --->


	<!--- Catch bad request errors. --->
	<cfcatch type="BadRequest">

		<!---
			Set up the error data. Notice that we are using the
			errorCode in the CFCatch exception to provide more
			insight into the error.
		--->
		<cfset request.statusCode = 400 />
		<cfset request.statusText = "Bad Request" />
		<cfset request.errorCode = cfcatch.errorCode />
		<cfset request.errorText = request.errorCodes[ cfcatch.errorCode ] />
		<cfset request.errorDetail = cfcatch.message />

	</cfcatch>

	<!--- Catch any unexpected errors. --->
	<cfcatch>

		<!--- Set up the error data. --->
		<cfset request.errorCode = 10000 />
		<cfset request.errorText = request.errorCodes[ "100000" ] />
		<cfset request.errorDetail = cfcatch.message />

	</cfcatch>

</cftry>


<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->


<!--- Set the response headers. --->
<cfheader
	statuscode="#request.statusCode#"
	statustext="#request.statusText#"
	/>

<!---
	Check to see if any errors were raised during the request
	processing. If so, we need to override the response value with
	an error response.
--->
<cfif request.errorCode>

	<!--- There was an error. Create an error object. --->
	<cfset request.response = {} />
	<cfset request.response[ "statusCode" ] = request.statusCode />
	<cfset request.response[ "errorCode" ] = request.errorCode />
	<cfset request.response[ "errorText" ] = request.errorText />
	<cfset request.response[ "errorDetail" ] = request.errorDetail />
	<cfset request.response[ "resource" ] = cgi.script_name />
	<cfset request.response[ "verb" ] = cgi.request_method />

</cfif>

<!--- Serialize the response. --->
<cfset serializedResponse = serializeJSON( request.response ) />

<!---
	Stream response to the client as a JSON body.

	NOTE: We are using "text/plain" rather than "application/json"
	so that we can more easily render the response in the browser
	for the demo.
--->
<cfcontent
	type="text/plain"
	variable="#toBinary( toBase64( serializedResponse ) )#"
	/>

As you can see, I have a struct, request.errorCodes, that contains the possible errors that will be raised by the API. I then have CFCatch tags that are designed to catch high-level CFThrow errors and translate them into more specific error response data.

The one resource for this API is defined by the process.cfm controller. As you will see in the following code, it halts processing the moment it finds an invalid data point.

Process.cfm - Our API Resource Controller

<!---
	Resource expects:

	name: String
	age: Numeric (0 - 99)
--->


<!--- Check to see if the name is supplied. --->
<cfif !structKeyExists( url, "name" )>

	<!--- Stop any further processing of this resource. --->
	<cfthrow
		type="BadRequest"
		message="The Name attribute is missing from the request."
		errorcode="11011"
		/>

</cfif>

<!--- Validate name value. --->
<cfif (
	!len( url.name ) ||
	(len( url.name ) gt 40) ||
	reFindNoCase( "[^a-z\s'-]", url.name )
	)>

	<!--- Stop any further processing of this resource. --->
	<cfthrow
		type="BadRequest"
		message="The Name attribute must be between 1 and 40 characters and may only contain letters, spaces, dashes, and apostrophes."
		errorcode="11012"
		/>

</cfif>

<!--- Check to see if age is supplied. --->
<cfif !structKeyExists( url, "age" )>

	<!--- Stop any further processing of this resource. --->
	<cfthrow
		type="BadRequest"
		message="The Age attribute is missing from the request."
		errorcode="11021"
		/>

</cfif>

<!--- Validate age value. --->
<cfif (
	!isNumeric( url.age ) ||
	(url.age lt 0) ||
	(url.age gt 99)
	)>

	<!--- Stop any further processing of this resource. --->
	<cfthrow
		type="BadRequest"
		message="The Age attribute must be a number between 0 and 99 (inclusive)."
		errorcode="11022"
		/>

</cfif>


<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->


<!---
	If we made it this far, the supplied resource attributes are
	valid. Now, let's create a fake resource for the demo.
--->
<cfset request.response = {} />
<cfset request.response[ "id" ] = randRange( 1000, 9999 ) />
<cfset request.response[ "name" ] = url.name />
<cfset request.response[ "age" ] = url.age />

As you can see, each point of validation uses a CFThrow tag with an errorCode attribute that maps to a key in the request.errorCodes collection. In addition to the errorCode attribute, I am also providing more verbose information in the message attribute. I figure this way we can break down the error even further, providing insightful debugging information.

Ultimately, I think the right long-term move is going to be making sure that the 3rd party API consumer can react to errors gracefully using the error codes. I think the additional message data should probably only be used to debug unexpected errors. Maybe. In any case, I really like the idea of being able to go beyond simple HTTP Status Codes; I think feature-specific errors codes are going to provide a huge boost to my API architecture.

Want to use code from this post? Check out the license.

Reader Comments

3 Comments

Excellent Ben, I liked the article :)

Just a tip (I think) is a best practice to add a status flag in the response even if all was good:

JSON: {
id: 5820,
name: 'Sarah',
age: 37,
status: 'success'
}

Greetings from Mexico! :)

15,663 Comments

@Joaquin,

Hmm, I could definitely see that. I like the idea of there being some sort of always-accessible value that is consistent. If nothing else, it can make code easier to write for the API consumer.

That said, it might be nicer to have the status code in the Headers, like "X-Error-Code" or something. I say this only because for resources that are returned successfully, it might be strange to mix resource-specific and framework-specific code in the same JSON body.

Good thinking!

3 Comments

@Ben, @Edward

This is just an idea, some articles of RESTful I read says that...

But definitely I'm agree to mantain the JSON body apart of the API framework... :)

Can you share some RESTful articles?

61 Comments

Thanks Ben,
That makes it much clearer. I must admit I misunderstood what you were trying to say about the api regarding using cfthrow.

I particularly like the idea of custom error codes. I have some code I was working on last night that I wanted to send you before I saw this post that I think you might find interesting.

I might actually try incorporate this error code logic and ill put it up for you to have a look at.

15,663 Comments

@Joaquin,

I recently read the RESTful API Design Rulebook by Mark Masse:

www.bennadel.com/blog/2324-REST-API-Design-Rulebook-By-Mark-Masse.htm

Definitely will take a while for me to absorb; and, there were parts of it that I didn't agree with (or thought were overly complex). But, definitely a book that I would recommend. I plan to re-read it after I do some more experimentation with my current MVC (Model-View-Controller) experiment.

@Steven,

I think the approach has some value. Of course, the caveat being that (as you KNOW), I am still getting my feet wet with architecture stuff.

Spent a few days trying to get my MVC stuff up and running. SLOW process - not using a database as a means to 1) work on my computer at home and 2) force myself to not break cohesive concerns by querying data I shouldn't "know" about.

61 Comments

@Ben,
No worries. Honestly its a relief to find out you don't know everything ;-)

Im actually going to try do a short intro presentation on mvc architecture on ugtv or something similar. If you dont mind I'm going to take some of the questions you asked me, plus others from the comments sections here and incorporate them.

15,663 Comments

@Steven,

Ha ha, no problem. I'm a firm believer that if 1 person is asking questions, many more are also wondering, but not saying anything :)

1 Comments

Here's a very important side-effect when using cfheader's statusText attribute: caching and/or proxy/firewall servers can obliterate this value.

I did some very clear experiments today, and visibly saw that our smart caching network (NTT SCD, so Akamai perhaps as well?) rewrote the status text of our 500 error to the default "Internal Server Error" string. I could hit the server with two different domains, one which forwards through the caching servers and one which does not, and watch the two different strings get returned.

Trying the non-caching domain from inside our company's firewall, I again saw it rewritten back to "Internal Server Error".

So don't count on the statustext being available to e.g. ajax calls for special error trapping routines. Use a custom header instead.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel