Skip to main content
Ben Nadel at the MySQL NYC Meetup (Oct. 2024) with: Scott Stroz
Ben Nadel at the MySQL NYC Meetup (Oct. 2024) with: Scott Stroz

Rethinking Error Type Schemas And Naming Conventions In My ColdFusion Application

By
Published in Comments (5)

Over the last few years, I've spent a lot of time thinking about error chaining, the difference between throwing errors and reporting errors, and a general set of DOs and DON'Ts for managing errors in an application. But, I've never put much thought into an error type schema or a naming convention for the errors that I throw() in my ColdFusion applications. As such, every time I go to throw an error, I'm left feeling very shaky about the whole thing. I need to develop a standard that I can adhere to such that I can focus on the business logic and not get distracted by the less significant details.

At InVision, we do have one standard when it comes to throwing errors. And it's that every error that we throw in our ColdFusion application has to start with the prefix: InVisionApp.. This allows us to quickly differentiate errors that we throw explicitly from the errors that are thrown through other means (such as infrastructure, networking, and database errors).

When we catch and log these errors - the ones that start with InVisionApp. - we log them at the INFO level. This level doesn't get sent to our log aggregation service unless our "Debug Logging" feature flag is enabled in production (or the error is allow-listed). Since these are "expected errors", we don't want to create a lot of noise in the logs just because the application is doing "what it's supposed to do." This leaves the "unexpected errors" as the primary entries within our log aggregation.

Beyond this, we have very little consistency in how we throw errors. Or, more specifically, how we define the type attribute of an error. The most often-used error type in our ColdFusion application is probably:

InVisionApp.NotFound

We have some ever-so-slightly more informational types like:

InVisionApp.Validation.InvalidField

And then some more specific error types like:

InVisionApp.Forbidden.OverQuota.MaxProjectMemberCount

And, finally, some even more specific error types like:

InVisionApp.EnterpriseMigration.NotificationNotYetSent

The first two don't really tell me anything about what is going on. With the first one, something was not found; but, I have no idea what that something was or why it was being retrieved. And with the second one, clearly there was an invalid field; but, I don't know what that field was, why it was invalid, or what kind of operation was being performed.

The latter two start to contain more meaningful information that allows me to, at a glance, get a sense of why this error might be occurring. And, where in the code I can look for related logic.

ASIDE: Just about all errors in our ColdFusion application are logged with a stacktrace. As such, I always know where to look in the code to find the triggering error. However, being able to see that from the type would be additional, beneficial information to have.

Ideally, I want all my errors to start skewing towards the more informational format that we see in the latter two errors above. And, in a perfect world, I think every single error would be completely unique such that every log entry that shows up in our log aggregation could be unambiguously tied to a specific line of code. I understand that this is not possible; but, I want to skew in that direction as much as can reasonably be done.

I've been mulling this over in my brain for the last week or so and the best idea that I can up with so far is this general format:

InVisionApp.{{ context }}.{{ error }}

This is constrained in format; meaning, it has to be a dot-delimited list of length 3. But, it makes no other constraints about what "context" means or what "error" means. Naming stuff is hard - one of the hardest things in computer science. And, rather than worry too much about how things are named, I hope that by focusing on the format of the error, the selection of names will become easier (thanks to the constraint).

I'm thinking that the "context" could either be an "entity" or a "use case". So, instead of the generic errors like:

InVisionApp.NotFound

... I'd be throwing "entity" errors like:

  • InVisionApp.Prototype.NotFound
  • InVisionApp.Screen.NotFound

... or, when meaningful, throwing "use case" errors like:

  • InVisionApp.TransferPrototype.TargetUserNotFound
  • InVisionApp.ActivatePrototype.PrototypeNotFound

The more specific my errors become, the more easily I'll be able to handle error reporting at the user-facing level. When a "use case" fails, I'll actually be able to share more information about why the use case failed; and, be more helpful in telling the user what they can to do (if anything) in order to remedy the situation.

What this may require me to do is, as needed, catch lower-level "entity errors" and then wrap them in "use case" errors. So, for example, when trying to perform the "activate an archived prototype" use-case, I might have to do something like this (pseudo-code):

<cfscript>

	public void function activatePrototype(
		required numeric authenticatedUserID,
		required numeric prototypeID
		) {

		try {

			var prototype = dataAccess.getPrototypeByID( prototypeID );

		} catch ( "InVisionApp.Prototype.NotFound" error ) {

			throw(
				type = "InVisionApp.ActivatePrototype.PrototypeNotFound",
				message = "Archived prototype not found.",
				extendedInfo = prepareErrorForWrapping( error )
			);

		}

		if ( prototype.userID != authenticatedUserID ) {

			throw(
				type = "InVisionApp.ActivatePrototype.UserDoesNotOwnPrototype",
				message = "Users can only activate prototypes that they own.",
				extendedInfo = serializeJson( arguments )
			);

		}

		if ( ! prototype.isArchived ) {

			throw(
				type = "InVisionApp.ActivatePrototype.PrototypeAlreadyActive",
				message = "Only archived prototypes can be activated.",
				extendedInfo = serializeJson( arguments )
			);

		}

		// .... truncated ....

	}

</cfscript>

Here, I'm catching the low-level InVisionApp.Prototype.NotFound error and I'm transforming it into a use-case-level InVisionApp.ActivatePrototype.PrototypeNotFound error. This is a bit more work, obviously. But now, when I go to report this error to the user, I can easily create an error translation layer that takes the type of error and returns a meaningful, user-friendly error message.

What's more is that I can now decouple the error from the "HTTP response". Meaning, this code snippet here doesn't speak to a 404 Not Found response or a 400 Bad Request or a 409 Conflict or a 403 Forbidden. This use-case code just emits the "issue" and then my centralized error translation middleware will worry about how that actually gets turned into a meaningful HTTP response with a user-friendly error message.

ASIDE: The message associated with a throw() expression is intended to be logged and consumed by engineers, not by users. As such, you should never worry about making a throw() message "user friendly"; nor should you ever use the message value as part of a subsequent translation. In many cases, you may not even need the message field as the type field will likely be sufficient. That said, I like seeing the message field in my log aggregation (your mileage may vary).

Commands vs Queries

When it comes to error handling and reporting, I believe that different levels of granularity are required by different types of requests. Specifically, I believe that "commands" - ie, requests to change the state of the system - should have better, more granular error handling. And that "queries" - ie, requests to read the state of the system - can get by with much more generic, high-level error handling.

When a user issues a command to mutate the state of a system, more meaningful feedback should allow the user to take steps to fix any issues that arise. However, when a user issues a query to read the state of a system, it's much less likely that the user can take any steps to fix those issues. As such, there's less of a need to report those issues with low-level granularity.

With all this said, I'm working with a brown-field application that has a decade's worth of code. So, I'm not about to start retrofitting this idea into existing workflows. But, I will start to try and use this error-type naming convention in net-new code and see how this pattern feels. I suspect that it will act as a "forcing function" that gets me to create more meaningful error management and error reporting in my ColdFusion application.

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

Reader Comments

45 Comments

Adding this thought I replied with on Twitter. I wouldn't want to define a class for error types like java does, but I do wish CFML allowed me to have a hierarchy of error types like Java where you "catch" IOEception and then I can extend that with a BradIOException that still counts as the former.
Plus, it really bums me out that CF engines don't allow exception to have a "caused by" so I can catch a low level error and rethrow it with additional info wrapped around it. I really like that from Java.

I asked both CF engines for that one 6 years ago :/ Such a shame how much worthless (IMO) development has been done but something simple like that is ignored.

https://luceeserver.atlassian.net/browse/LDEV-438
https://tracker.adobe.com/#/view/CF-3842667

15,902 Comments

@Brad,

1000% I would love the ability to nest errors as well. Today, I hack that in using the extendedInfo. Basically, I will often jam a low-level error into the extendedInfo using serializeJson( low_level_error ). This makes the errors harder to read in the log aggregator. But, I built a little UI that will recursively deserialize JSON :D

But I agree that I wouldn't want to create classes for everything. I've come to use the type for that kind of thing, and then just accepting that some of my error reporting is going to be limited.

45 Comments

Yes, same here. But extended info even pissed me off because it MUST be a string! WHY? Why can't extended info be a struct or an array?

As far as sub types, the Coldbox framework for example will use a convention where have a namespace with dots like

  • SES.InvalidNamespaceException
  • SES.IncludeRoutingConfig
  • SES.InvalidModuleName

What's pretty cool is I can catch just "SES" as a type like so:

try{
	throw( type='SES.InvalidNamespaceException' );
} catch( SES e ) {
	writeoutput( 'SES Error caught<br>' );
}
writeoutput( 'Complete!' );

I'd say it's the next best thing for error "inheritance".

15,902 Comments

@Brad,

I think it's nice that we can catch on dot-delimited prefixes in the code. But, I haven't really worked that into my error handling yet in any way. Over on Twitter, Mingo Hagen was talking about his error-type schema, and how he puts things like validationError on the front so that he can catch those sub-type of errors.

I like it in theory; I just don't know how to integrate that stuff just yet.

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