Rethinking Error Type Schemas And Naming Conventions In My ColdFusion Application
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 athrow()
expression is intended to be logged and consumed by engineers, not by users. As such, you should never worry about making athrow()
message
"user friendly"; nor should you ever use themessage
value as part of a subsequent translation. In many cases, you may not even need themessage
field as thetype
field will likely be sufficient. That said, I like seeing themessage
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
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
@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 theextendedInfo
usingserializeJson( 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 :DBut 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.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:
I'd say it's the next best thing for error "inheritance".
@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.
@All,
Over the last few days, I've massively refactored the error-handling on this blog, and am now using hierarchical type conventions inspired by the thinking here:
www.bennadel.com/blog/4173-centralizing-the-error-response-handling-for-my-coldfusion-blog.htm
But, I also talk about catching exceptions and translating them into error responses that are safe to show users.