Exceptions Are For Programmers, Error Responses Are For Users
I have - quite literally - spent the last few months (and maybe years) mulling over the topic of error handling. After writing a lot of pseudo-code and reading dozens of articles on the matter, I think I am beginning to build a half-way decent mental model for how exceptions fit into my own personal application development style. Just recently, however, the blog post, Exception messages are for programmers by Mark Seemann gave me a bit of an "ah ha!" moment. Mark's article brought clarity to my mind that there should be a strong separation between the exceptions thrown by the application and the error responses that are sent back to the user or client.
Right now, I am in the "exceptions are good" camp. While I understand that exceptions do have more computational overhead than return codes, my current mental model holds the benefits of exceptions as outweighing the drawbacks. Over time, this may change - but for now, I am trying to figure out the right way to involve exceptions rather than trying to figure out how to remove exceptions from my code.
That said, exceptions do make me uneasy. Specifically because I am never quite sure what information I should provide to a raised exception. On one hand, I want to provide as much (if not more) information than is needed so as to facilitate future debugging. But, I also get nervous that some of this information may leak out to the user. So, I end up trying to think about the exception - especially the error Message - from the user's point of view.
The problem with this thought process, though, is that I have begun to conflate the idea of an "exception" with the idea of an "error response". And, this is exactly the problem that Mark Seeman's article made clear. Exceptions are not error responses. Exceptions should never be returned to the user. These statements sound obvious, I know; but, saying them makes it apparent that exceptions are for programmers, not users. And, that I should never worry about making exceptions "user friendly" or trying to limit the amount of information that I expose in an exception.
Ultimately, however, we do have to return a response to the User. And, unless we want to return nothing but "500 Server Error" responses, we do have to build some sort of translation layer that converts "exceptions" into User-oriented "error responses":
|
|
|
||
|
|
|||
|
|
|
Here, we have a hook in the web application - the "delivery mechanism" for the application core - that catches exceptions and translates them into error responses that are safe to return to the user. In order for this translation to work, the translator needs to be able to correlate an exception to an error response. This correlation is likely to be somewhat technology-dependent; but, in my current approach and technology choices, I am using the exception's Type property (ex, "UsernameEmptyException") as the correlation ID.
As a result of this organization, not only do we have a strong separation of concerns but, we also centralize all of the errors that the user can receive. As David Abrahams points out in his article on Error Handling, this provides excellent documentation and hooks for features such as Internationalization (i18n):
Don't worry too much about the what() message. It's nice to have a message that a programmer stands a chance of figuring out, but you're very unlikely to be able to compose a relevant and user-comprehensible error message at the point an exception is thrown. Certainly, internationalization is beyond the scope of the exception class author. Peter Dimov makes an excellent argument that the proper use of a what() string is to serve as a key into a table of error message formatters.
Exception handling has many facets. And, I'm still trying to figure out things like how to handle exception chaining and how to integrate exception-specifics into error responses. But, I think creating this strong separation between exceptions and error responses was a huge leap forward for my mental model. More goodness to come!
Reader Comments
I've run into an issue with CF2016's debug/error output. Essentially, we want to not use the global error template on a particular site, thus enabling debug error output for the IPs defined in the CF Administrator.
We'd rely on a <CFERROR> tag to send non-dev users a custom error template.
The problem is that in certain cases, the page will spit out error information before the <CFERROR> tag is processed (and not just when an exception is higher up in the code), thus dumping out error info to non-dev users!
In the past, I was able to work around this by editing the built-in detail.cfm error template and force an IP check there (against the IPs stored in neo-debug.xml). In CF2016, this doesn't seem doable (or at least, not easily).
I've put more details and a simple example here: https://forums.adobe.com/message/9508057#9508057 Perhaps I'm missing something simple?
@BW,
To be honest, I haven't used the CFError tag in a long time, not since the Application.cfm template switched over to the Application.cfc component. Since then, I've moved to use the onError() life-cycle event handler in the ColdFusion Application.cfc component. With onError(), I can control the output explicitly.
Sorry I can't be more helpful with your specific situation.
That's a really important part to bring up. As a developer, good exception handling is crucial for building a robust app. This is closely coupled to error messages to a user, because if an exception occurred, you're likely going to need to tell them something, but it is a separate problem to solve.
That translation seems a great centralized solution to it. Not only does it allow you one place to map exception to message, but also a place to decide what ones require messages versus a response that may require logic like redirecting to an error page. That may be outside the scope of "translation" but I can see the usefulness to that also being centralized.
Does the onError event handler properly handle a situation where an exception is thrown prior to page execution (presumably during a parsing pass of some sort)? I've used onError in the past and from what I remember it had the same problem.
For example, a stray, open CFIF tag somewhere on the requested page (or any included page or function) would result in an "Invalid CFML construct found at line..." error, with code leakage. Even if onError works, my guess is it could still break if such errors existed within the application.cfc page.
I consider this a bug with the debugging/error output in general, as it's not respecting the IP list defined in CF Administrator, and some settings in the debugging output section affect what's shown in the error output.
I'll do some more testing and see if I can prevent the leaks.
It looks like my workaround is going to be as follows:
1) Use the "Site-wide Error Handler" in CF Administrator (Server Settings, Settings, Site-wide Error Handler).
2) In the specified template, check against the IPs in the neo-debug XML file (\cfusion\lib\neo-debug.xml under the CF installation directory).
This file contains the IPs specified in CF Administrator as being able to see debug output. XMLParse and CFWDDX can get you the IPs as an easy-to-use list.
3a) For IPs not in the neo-debug.xml file, kick show our generic error template.
3b) For IPs in the neo-debug.xml file, simply include the detail.cfm file ( \cfusion\wwwroot\WEB-INF\exception\detail.cfm under the CF installation directory).
If you change the "Enable Request Debugging Output" setting, make sure your custom template is still defined for the "Site-wide Error Handler".
In my testing, this achieves exactly what we want:
CFError still works as before and handling can be customized per site/application/whatever. Presumably OnError also works.
Errors that cause processing to halt before CFError is processed are handled by the "Site-wide Error Handler" template. Debugging IP Addresses we set in CF Administrator get redirected to the generic template with the info they expect. Everyone else gets a generic message.
@John,
I think the translation can be more or less dynamic as needed. For example, when generating the error "response" for the user, you might even be able to pass the Error object into a method so that the translation layer can be more insightful:
var response = getResponseFromError( error )
... then, the Error properties can be used to build the response.
For example, when dealing with Recurly - a payment processing gateway - they actually return a Message that they tell you you _should_ pass onto the user. So, in that case, one can imagine a translation layer that plucks properties out of an Error of type Recurly:
if ( isRecurlyError( error ) ) {
. . . . response.message = error.message;
}
... which still keeps the translation centralized, but couples the response to the error structure a bit more.
@BW,
You raise a good point (no pun intended). I believe that error handling and try/catch blocks only allow you to handle runtime errors. Compile-time errors, like invalid syntax in a template are not the same and won't be handled in a try/catch block, or perhaps OnError() handler -- I'm not sure on the latter.
@Ben,
Yeah, it's great for that. If you have the error object itself, you have one place to handle that logic: is it something I map, does it have a server error I need to return, or is it something that I can log and pass on?
I really like this idea and am going to look into implementing something like this on the project I'm working on. I already have a service (we're in Angular 4.1) that all components can put in messages to be displayed as toasts, and then I have a single component that subscribes to it and will display them, so you get stacking toast message, that auto hide / closeable, etc. I'm pretty happy with the end implementation. No reason errors cant do something similar.
@John,
That sounds really cool! I like the idea of the toasters - I haven't played around much with that UI pattern yet.
While I've done the translating of Errors into "Responses", I have yet to actually make the content of the message dynamic based on the error itself (other than Type -> static message). I think that becomes a bit easier if you have different classes for your errors, so that you can *depend on* the structure of that error. But, right now, I just use a single "AppError" class that allows you to add an arbitrary ".extendedInfo" hash. But, the problem is, since it's open-ended, it's tough to depend on the structure (such as the consistent naming of properties). I'm still noodling on that, to see if I can make that easier to consume from a dynamic-message standpoint.
I work on the exceptionless project (https://github.com/exceptionless/Exceptionless) and run into this thought process quite a bit. What we've found works the best is to use reference ids associated to an exception instance and return these to the users (https://exceptionless.com/using-reference-ids-for-better-customer-service/). Then we show this reference id so we can look it up if it's sent to us. I think friendly error messages should be put in almost all cases in the ui logic around where the call took place to a service. This way it's disconnected from the implementation. If you must return it from the service, make sure it's a generic friendly message that can be shown to the users.