Earlier today Seth (@CapedCoder) mentioned something on Twitter that I thought was a bit odd. He was looking for a way to disable try/catch functionality for dev versus production. Basically, "Don't try/catch in dev." This led to a few emails back and forth where I made the assertion that I thought he was using try/catch wrong. Not being the end all of things - and knowing I have smart people here - and knowing that this question was bigger than ColdFusion, I thought it would be a great topic of conversation for the blog. So - let's get to it.
First off - technically - there is no real way to just disable try/catch. You could, though, do something like this:
<cftry>
<cfset makeLemonadeOutofCrap()>
<cfcatch>
<cfif application.isDev>
<cfrethrow>
</cfif>
Sorry - unable to perform this action.
</cfcatch>
</cftry>
It requires more work than simply "disabling" cftry, but it does fit the bill for the request. However, I think this is bad approach and I think it comes down to the cases where we need global exception handling (via cferror, onError, etc) versus try/catch. In my mind, they are two very different things:
- Try/Catch should be used in cases where you expect an error and have no possible way to work around it. Your code basically says, "There is no way I can possibly know this is going to run safe, and I expect it mail fail, so please be ready for an error."
- On the flip side, Global Error handling is for the unexpected error. It's the side cases you didn't test for. (You did test, right?) It's the guy messing with URL parameters trying to force an error.
In the try/catch scenario, you expect an error and should/could handle the error in a nice way. But as it is expected, why would you do anything different for development versus production? You certainly wouldn't want to force a new exception as that's not how your application runs in production, and in general, you want things to be as similar as possible.
I really thought I had a bit more to say here - but I think I'm out of ideas. Mainly his approach just "felt" wrong, but I'd love to hear what others think. And again - I don't see this as a ColdFusion question at all. I'd assume you could apply the same thinking to PHP, Ruby, etc.
Archived Comments
Ray, I believe your description of Try/Catch contrasted to global error handing mechanisms is spot on and I would certainly have Try/Catch on in development and test-QA and production; for instance it might be a call to an external resource which could be suspected to fail wherever the code is.
I don't think the two are mutually exclusive. There are times you might want to log particular exceptions around critical areas of code and then just rethrow for the global handler to catch. There may also be times where you _don't_ want to have the global handler involved and you would rather have the try catch handle it (I'm thinking of remote call exceptions). I think a healthy application will make use of both types of exception handling.
People, I think, misunderstand the best use of try/catch. It's NOT to catch errors, but to protect your code from things you have no control over.
Here's some code...
try{
var length = Len( arguments.someValue );
} catch ( any e ) {
var length = 0;
}
Good? No. Here, you do have control over someValue.
If you're worried that someValue might not exist in the arguments, provide a default value.
If you're worried that the data type for someValue might not work with the Len() function, enforce data typing.
Here is a better use for try/catch:
try{
var length = Len( new model.SomeClass().someValue() );
} catch ( any e ) {
// appropriate error-handling
}
Since you don't have control over SomeClass's someValue method, you may want to protect your code.
More can be said about try/catch, but I think if someone were just to stick with that simple idea ("Do I have control or not?"), they could go a long way.
What better way to test your error handling than to use try/catch during development? By the time my code goes to production, my error handling has been thoroughly tested, so I'm pretty confident that if something goes wrong, the user will be notified, the error will be logged, and I'll get an email.
One thing that is worth mentioning... my error handling is smart enough to know when I'm working in a dev environment, so it doesn't log/email the error information.
@Steve
I find the issue of logging (on during dev; off otherwise) can be handled better by leaving calls to the logger in place and replacing the actual *logger*.
In dev mode, I have a TrueLogger that has two methods: msg and log. These log to a Development.log file.
In production mode, I switch TrueLogger out for FalseLogger that also has two methods: msg and log. These, however, do nothing and cost (almost) nothing.
One upside is that if something wacky starts happening in production, you can swap out FalseLogger for TrueLogger and actually see what's going on in production.
At least, that works for me.
@Hal While I agree with your use of a global logger to protect code from "external influences", I was thinking more about needing to take special action when something fails within the application (perhaps a failed call to an ESB or remote endpoint).
Additionally, if my application provides an external API of its own it might not be appropriate for the global handler to respond to remote calls. In a perfect world the API would be an application of its own, but I imagine we can all put our hands on an application within minutes where that is not the case.
For the latter scenario one could argue that the global handler could be smart enough to understand the differences between a remote and an internal application call. That, however, makes me think back to something you told me a few years ago about not creating "magic bullet solutions" and I think that applies here.
At the end of the day I think it comes down to what you stated about code protection _and_ context of the requesting source.
Too often I find that try and catch do little more than suppress errors that *should* surface and get in the way of fixing bugs...and there is almost never a global way to over-ride that.
I like Hal's way of handling things, especially the part about being able to turn on logging later (without setting the whole site to "isDev") and still having the code fail gracefully on the front-end.
As most people have mentioned the deciding factor is if you want the processing to stop or to continue.
When using any external HTTP calls via CFHTP or WebServices or even when using a 3rd party API or even an API you are creating, you will need to decide whether to allow the continued page request or abort and display the error.
Most notably it goes without saying as others have said, that it would be a given to try/catch a Web Service call and decide based on the info returned from that call will decide on what you need to do.
API's or your own framework, is a case where you might want to throw an excpetion. Then your application can decided on what to do with that exception, examples here would be say I am writing a database upgrader. One of the methods is to excute code that does some checking and reports that it can't find a file/database, you would throw a custom excpetion.
Once this falls back to the loop that is processing the files, you might decide that the importance of stopping the process is not important enough for a missing file. That means you can capture, check the type of exception and log the error and allow it to fall silently back into the loop and continue.
There are no hard and fast rules here, and it will be what you as a developer really want the application to do. If you are lazy enough to not even think about all possible scenarios, then you might be very comfortable in not using the try/catch to do what you need and just let it happen and deal with it when it happens.
Here's how I would *handle* this in CF speak. The main objective is to always throw or re-throw errors to the global error handler, even from OnError(). Then in the development environment simply turn off the global error handler, or, from in the global error handler do an environment check or allow the global error handler to be enabled or disabled using IsDebugMode().
This allows applications to use try/catch as necessary to handle errors as required, but the global error handler remains the user friendly catch all.
Additionally, unless there's a specifc need to know where an error was caught, I avoid using cfrethrow. Instead I use cfthrow object='#cfcatch#' which preserves the original error as is, effectively hiding the fact that the error has been re-thrown.
Thanks to all, I have a much better understanding, and some great ways to approach the problem I was trying to solve initially.
@Hal, the TrueLogger/FalseLogger is a great idea - I had been leaving the logging on (though just for error events). I will definately be asking myself if I control it or not in the future and taking the appropriate action.
@Sharon and @JP, I think my biggest issue was with overthinking and trying to get a bit too granular (and OCD) in my catching/logging.
@Andrew, @Mike, @Steve, I think your examples are a perfect place to implement Ray's solution and have some sort of application variable where you can watch for certain errors (remote for instance) and then either rethrow, log, or make lemonade.
@MrBuzzy and @Ray this is definately the direction I am going to head from now on. Let the global error handler do its job, that's where the logic belongs.
Actually I was leading away from the flag option.
The reason being is that when you begin getting in the habit of using the try/catch block, and understand when to use it or need it, that the global flag is really going to a hinder more than anything.
The reason I make that is because you might need to capture and do a redirection in work flow, by turning this of on a global level this will never happen and will cause your application to maybe behave incorrectly.
One should always think about looking at what they are doing, so if you might be doing say file IO. In a normal world we would need to write try/catches to make sure we alert our caller, that an error has occurred. This is normal analytic thinking about the situation, if you don't try/catch this then later on down the track you are going to be patching this to fix the errors that only sometimes crop up.
But if you write a flag to override this, then you cause issues that you might normally not get.
The safest thing to think about here is what is the code block going to be doing, and is there a potential that what I am calling might throw an exception. This is where Java and ColdFusion are different, in Java you are not going to be able to compile any code that has not caught an exception, when it calls something that requires this. ColdFusion doesn't care, it will just eventually fall down to the global error handling level.
So what I am saying is that you should be forgetting about that level, if it is for logging purposes to notify you of the errors. Then yes I would look at some flag that defines the difference between dev and production, and then provide a service / AOP to handle providing the information that you require, or minimal as required in your logs dependant on what server you are on or even require.
Hope that helps some more.
@Andrew, sorry for confusion. What I was getting at was the possibility of having different types of exception flags. Maybe you have one for remote calls only then you could determine what the catch should do depending on dev / prod. The app is almost always going to rethrow I imagine, unless its an external call in which you may just create an empty result struct for instance and log the fact that the remote source could not be contacted. This would allow the app logic to continue (albeit with an empty resultset), but at least you would be alerted that the webservice was down. In dev mode you may want to turn that off so that you can dump or log the resultcode and try to determine what is going awry.
I suggest writing an exception handling cfc which you can use to log errors in a catch or in the global exception handler. This way you have the option of logging the error and continuing as normal.
@MrBuzzy My testing in Adobe Coldfusion 8.0.1 shows that using <cfrethrow> doesn't change the exception at all and it has the advantage preserving the stack trace (AKA tagcontext). Is there a case when <cfrethrow> changes the exception?
See now you just raised another issue.
I always without fail make sure that I capture any type of exception, that is raised via a remote call. And respond with custom error messages that anyone using the API calls can then do what they need too, this is regardless of whether that call will be made from a remote call or not. This way I am writing one block of code, and not writing one for internal use or just one for external use.
The point is that when you start mixing this type of behaviour, you can't expect a global option to just switch it on or off and will stand by this should not be decided on whether it is in production or not. Logging information for later analysis is a different story.
I also live by the rule that you want to mimic your production with your development/testing environment as much as possible, there are limits to this rule. But when it comes to code, then the rule is simple unless it is logging information, then if you expect to debug the code then you are going to be debugging this on your machines. Anyone who debugs on production is only asking for more complaints from your customers/client.
Seth, I am not just trying to get you to think outside the square. The try/catch block is there for a reason, and it might very well work right now for you to think and use this way. Now I know you might be thinking that you want to run code that is on production differently to development, and a flag will work very well there. But when it comes to dealing with exception handling then no, I disagree with a global flag option on that alone.
@Hal I actually use try/catch precisely for things I DO have control over, which is kind of what Ray was getting at.
Best use case I can think of is I have an app that does a ton of backend processing of data that comes from the wild, so as you can imagine I have to reject it for about 50 different reasons but I need to know precisely the reason why the data needs to be rejected and handle completely differently depending on the reason.
The key here is a single try statement can have multiple catches, and remember that you can explicitly *throw* an error of a custom type as well, which makes for some slick stuff in a layered architecture.
So let's say I have a controller that calls a service, and what goes on in the service might fail for one of my 50 aforementioned reasons.
In the service I might have something like:
... do stuff ...
<cfif errorState1>
<cfthrow type="errorType1" />
</cfif>
... do more stuff ...
<cfif errorState2>
<cfthrow type="errorType2" />
</cfif>
And then in my controller:
<cftry>
<cfset callServiceMethod() />
<cfcatch type="errorType1">
... do stuff ...
</cfcatch>
<cfcatch type="errorType2">
... do completely different stuff ...
</cfcatch>
</cftry>
I've found that works extremely well and makes the service code simpler because it doesn't have to do anything other than say "I can't proceed, here's why, whoever called me needs to deal with it." And of course if the error isn't caught it bubbles up anyway and ultimately will be dealt with at a higher level.
Anyway, just wanted to chime in with that example because I've found it works really well for me. Basically keep error *handling* as far "forward" (if that makes sense) in your application as possible, and further down I tend to just throw errors (and make use of the ability to throw custom types!) and let the caller handle it however they see fit.
@Matt - nice example.
BTW saying that slick stuff can be done with custom excpetions is also an understatement.
@Marc if I remember correctly, it adds additional lines to the java stacktrace. Also if viewing the error in the browser, with 'show detailed ...' turned on, you will see the CFML from where the error was rethrown, not the original place the error occurred.
I agree, @Matt. They can also be used for flow control, if you're very careful about what you're doing.
Wow - I spent the night "away from desk" and come back to a great set of comments.
First - Hal and Andrew - I really like how you guys described when to use try/catch. Andrew, I especially liked your simple "if you want the processing to stop or continue" description.
@Steve: To your first comment, I hope my text did not imply one should be used -instead- of the other. I'd assume 100% of apps out there make use of a global error handler and many would also need try/catch blocks somewhere in there.
@Sharon: Yeah, I ran into an improper use of try/catch a few weeks ago. I was shocked as it was a pretty important process and all errors were simply being ignored.
@MattW: I didn't think I implied I'd use try/catch for things I have control over - but that being said I think your example makes sense too. (And this is exactly the type of comments I was hoping to see. :)
I find that another good use for try/catch is inside of a cfthread where the standard global error handling is not going to alert the user that something went wrong. You might end up rethrowing, but you'll be able to at least alert the user at some point that there was an error.
Regarding throwing custom error types, I'm reminded of a post a while back from Mark Mandel, which is an approach I like:
http://compoundtheory.com/?...
Have a peak at the ColdSpring 2.0 code repo over at SourceForge for some examples of this in action.