Skip to main content
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: Valerie Poreaux
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: Valerie Poreaux

Overloading Error.ExtendedInfo As A Data URL In ColdFusion

By
Published in

For the most part, ColdFusion provides wonderful error handling functionality. Between the try / catch / finally blocks, the throw() statements, and the global error handler (in the Application.cfc), it's hard for any error to go unnoticed in a ColdFusion application. But, one thing that isn't so easy to do is provide additional complex data alongside a given error. Historically, I've overloaded the .extendedInfo property in order to provided such additional information. And, that's what I'm talking about in this post. But, this post is a refinement on the idea, making the technique significantly more robust and consumable.

In the past, if I wanted to included root cause information along with an error, I'd serialize some set of the root cause properties as JSON and store them in the extendedInfo attribute of the throw() statement:

<cfscript>

	try {

		// Attempt to duplicate user ...
		throw( type = "UserNotFound" );

	} catch ( any error ) {

		throw(
			type = "UserDuplicationError",
			message = "User could not be duplicated.",
			extendedInfo = serializeJson([
				type: error.type,
				message: error.message
			])
		);

	}

</cfscript>

Similarly, if I wanted to include some validation metadata information along with an error, I'd serialize it as JSON and store it in the extendedInfo attribute of the throw() statement:

<cfscript>

	stub = "this-is-my-rather-long-stub-what-what";

	if ( stub.len() > 20 ) {

		throw(
			type = "Stub.TooLong",
			extendedInfo = serializeJson({
				maxLength: 20
			})
		);

	}

</cfscript>

The major problem with this approach is that the error handler at the boundary of my application has no way of differentiating the type of data stored in the extendedInfo property. It can determine if the given value is a valid JSON payload or not; but, it has no way of understanding what the embedded JSON represents.

The other day, as I was embedding an SVG image inside a CSS background-image using a data URL, it occurred to me that I could be treating the .extendedInfo property like a data URL as well.

A data URL is a special string that contains an embedded file. The client (a browser in the CSS case) is able to determine what type of file is embedded by inspecting the prefix of the string. For example, in my embedded SVG scenario, the data URL started with:

data:image/svg+xml;utf8,

I can use this kind of prefix concept to allow my ColdFusion application to differentiate .extendInfo payloads. For a root cause payload, I can use:

data:cause/json,

And for a validation metadata payload, I can use:

data:metadata/json,

Both payloads will be JSON; but, the data URL prefix makes it clear what that JSON represents.

Since throwing an error and catching an error takes place is different parts of the ColdFusion application, I want to centralize this logic in a reusable way. I've created an ErrorUtilities.cfc ColdFusion component that provides methods for:

  • Embedding various JSON payloads.

    • embedCause()
    • embedMetadata()
  • Differentiating various JSON payloads.

    • isCause()
    • isMetadata()
  • Extracting various JSON payloads.

    • extractCause()
    • extractMetadata()

For this exploration, I've chosen to use the extendedInfo property as the argument; but, one could easily update this code to accept the error itself as the argument, which might make the encapsulation of logic a little stronger.

component
	output = false
	hint = "I provide utility methods for generating and consuming errors."
	{

	/**
	* I initialize the error utilities.
	*/
	public void function init() {

		variables.causePrefix = "data:cause/json,";
		variables.metadataPrefix = "data:metadata/json,";

	}

	// ---
	// PUBLIC METHODS.
	// ---

	/**
	* I serialize the given error as the root cause to be embedded as an extendInfo
	* property overload.
	*/
	public string function embedCause( required any rootCause ) {

		var liteCause = [
			type: rootCause.type,
			message: rootCause.message,
			detail: rootCause.detail,
			extendedInfo: rootCause.extendedInfo
		];

		return ( causePrefix & serializeJson( liteCause ) );

	}

	/**
	* I serialize the given struct as the metadata to be embedded as an extendInfo
	* property overload.
	*/
	public string function embedMetadata( required struct metadata ) {

		return ( metadataPrefix & serializeJson( metadata ) );

	}

	/**
	* I extract the root cause from the given extendedInfo property overload.
	*/
	public struct function extractCause( required string extendedInfo ) {

		return deserializeJson( extendedInfo.right( -causePrefix.len() ) );

	}

	/**
	* I extract the metadata from the given extendedInfo property overload.
	*/
	public struct function extractMetadata( required string extendedInfo ) {

		return deserializeJson( extendedInfo.right( -metadataPrefix.len() ) );

	}

	/**
	* I determine if the given extendedInfo error property is being overloaded as a root
	* cause container.
	*/
	public boolean function isCause( required string extendedInfo ) {

		return !! ( extendedInfo.left( causePrefix.len() ) == causePrefix );

	}

	/**
	* I determine if the given extendedInfo error property is being overloaded as a
	* metadata container.
	*/
	public boolean function isMetadata( required string extendedInfo ) {

		return !! ( extendedInfo.left( metadataPrefix.len() ) == metadataPrefix );

	}

}

With this new ErrorUtilities.cfc ColdFusion component, let's refactor the earlier examples. First, the need to embed a root cause in a thrown error:

<cfscript>

	errorUtilities = new ErrorUtilities();

	try {

		try {

			// Attempt to duplicate user ...
			throw( type = "UserNotFound" );

		} catch ( any error ) {

			throw(
				type = "UserDuplicationError",
				message = "User could not be duplicated.",
				extendedInfo = errorUtilities.embedCause( error )
			);

		}

	} catch ( any error ) {

		writeDump( error );

		if ( errorUtilities.isCause( error.extendedInfo ) ) {

			writeDump(
				label = "Error Root Cause",
				var = errorUtilities.extractCause( error.extendedInfo )
			);

		}

	}

</cfscript>

As you can see, I'm using the .embedCause() method to serialize the root cause error. Then, I'm using the .isCause() method to introspect the error. And finally, I'm using the .extractCause() method to extract and deserialize the root cause information:

Let's now refactor the validation metadata example:

<cfscript>

	errorUtilities = new ErrorUtilities();

	try {

		stub = "this-is-my-rather-long-stub-what-what";

		if ( stub.len() > 20 ) {

			throw(
				type = "Stub.TooLong",
				extendedInfo = errorUtilities.embedMetadata({
					maxLength: 20
				})
			);

		}

	} catch ( any error ) {

		writeDump( error );

		if ( errorUtilities.isMetadata( error.extendedInfo ) ) {

			writeDump(
				label = "Error Metadata",
				var = errorUtilities.extractMetadata( error.extendedInfo )
			);

		}

	}

</cfscript>

As you can see, I'm using the .embedMetadata() method to serialize the validation information. Then, I'm using the .isMetadata() method to introspect the error. And finally, I'm using the .extractMetadata() method to extract and deserialize the validation information:

CFDump of an erorr and the extracted matadata associated with that error.

In both the root cause and the validation metadata cases, I'm overloading the built-in extendedInfo error property to contain extra, JSON-encoded information. But, by treating the extendedInfo property as a data URL, I can easily differentiate the type of overload; and, I can also determine if no overload is being used at all.

And, of course, once we have this prefix-based differentiation in place, it becomes easy to extend with any other type of overload that you might need in your ColdFusion application.

ColdFusion Strings All the Way Down

ColdFusion integrates very nicely with Java. So, when creating specialized errors, it might be tempting to go down into the Java layer and actually create Java classes that can be thrown using an instantiated Java exception:

throw( object = new MySpecialJavaError() )

This idea is fun in theory; but, I think it has a lot of drawbacks, not the least of which is that now you have write Java code (which is exactly what ColdFusion is abstracting away).

Ultimately, however, the biggest drawback of creating custom Java classes for errors is that errors in ColdFusion traditionally use strings for all properties. Which means that your entire error handling and logging apparatus can rely on errors being "bags of strings" that are easily serializable. Using JSON to embed special payloads works with the grain of ColdFusion, not against it.

Of course, if you love Java, your mileage may vary.

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

Reader Comments

Post A Comment — I'd Love To Hear From You!

Post a Comment

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