Bob asks:
I've been googling for a couple days and can't find an answer to this. I have a function in a CFC with an argument of type numeric. If the value passed to the function is not numeric, an error is thrown. How can I catch that error? I tried wrapping the cfargument tags in a try/catch block, but CF doesn't like that. I'd like to handle it in a way that I can return a default value if the proper value isn't passed in.
You've got a couple things in play here, so let me address them one by one. First, you noticed that you couldn't wrap your cfargument tag in a try/catch block. That is by design. When it comes to CFC methods, there are two 'special' types of code blocks that must placed in particular order.
The first is cfargument tags. The second is var statements. In both cases, you can use CFML comments anywhere. But the important thing here is to remember that cfargument tags have to be used first and you can't mix any other tags in there. Even a simple CFIF is not allowed.
So given that he wants to use a default value when a non-numeric argument is passed, how can he handle it? First, he has to change his cfargument tag to be looser in terms of validation. His current code probably looks like so:
<cfargument name="raysage" type="numeric" required="true">
By it's very nature, this line says that the argument, raysage, must be passed and must be numeric. Period. In order to change it to allow for non-numeric values, you would simply do:
<cfargument name="raysage" type="any" required="true">
Then, after any other cfargument or var statement, you would do:
<cfif not isNumeric(arguments.raysage)>
<cfset arguments.raysage = 0>
</cfif>
Obviously 0 isn't too important there. Whatever default you want to use should be placed there instead.
I shared the above with Bob and he responded with:
You know, something interesting to cover might be what the purpose of the type attribute is if you can't gracefully enforce it. For example, why would I set an attribute to type="numeric" if I can't do anything with that except blow it up?
My response to him was that his use of the argument is probably a bit unusual. Normally people want the argument check to be strict: You MUST pass it as type So and So. Period.
Most of the time people would not want logic like he had. That's why we had to use a bit more code to handle his specific business rule. The same would have applied if he had some other rule, like numeric, but greater than zero. Outside of type checking, anything else would be custom.
Archived Comments
Another thing to keep in mind is while you can't trap the error in the function itself, you can trap the error in the code that's calling the method.
In cases like this I often make a separate function which wraps the core one and tests the input before calling the main function. Barring that, I'd have stronger validation of the input before calling this function I guess.
@All: This was my issue and I appreciate Ray blogging on the topic. The function is being called from a bound cfinput element, returning an asynch result based on other form inputs (a small calculator app). I used javascript on the input fields to limit the keystrokes to numbers-only, but if you delete the field contents, the binding sends an empty string to the function, which is not numeric. Ray's solution worked perfectly, but it made me question the purpose of the type attribute in the AJAX world where you don't want a full-blown error distrupting the user experience.
In my opinion (which may be the same as Rays), specifying a strict argument type is really a debugging tool to catch coding errors. As Ray indicated, if the input (say from a user) can be either, then you would not use a numeric data type as an argument.
@Robert: Its a good question, and I often run into a similar issue. I skip the "type" attribute for <cfparam> for the same reasons you're having problems with the type on <cfargument>: if a user passes something unexpected, the system throws an error rather than letting you handle it. You can get around that issue with <cftry>/<cfcatch>, but why not just leave off the type attribute and handle the validation manually? Basically, the type attributes offer very strict validation, where some situations (specifically any ones that involve users) require fuzzier validation. In those cases its best to create something like myAjaxFunction() that handles the validation and calls myFunction() if everything is ok. This gives you both the strict version and a more user friendly version as well.
@Robert:
Another thing you can do, is stick to using strict validation for non-remote methods and then use a proxy CFC for all your remote methods.
While using a proxy requires a little more set up, it allows for a lot more control and allows you to set up better error handling.
I think Dan's point is the critical one here. The question isn't the merit of specifying argument datatypes. The initial problem had more to do with where the application is dealing with unexpected input. Exceptions thrown from bad argument types are meant to be handled in the consuming code, not in the function itself. The purpose of the exception is to tell the calling code "No soup for you!" It's the responsibility of the calling code to turn that into a friendly response. In Bob's case, he needed the function itself to handle the problem and not indicate failure to the calling code.
My preferred way of dealing with this is to catch the error in the presentation layer. I think it's important for functions to take a strict set of arguments and types. That way the function can be used to perform a specific operation and return a result, without having to be cluttered up with error checking (like everything there are some exceptions).
Take the example where you a UDF that calls another to perform an operation. That UDF may in turn call another UDF for something else. If you simply perform error checking at the presentation layer, you should only need to check the input variable once, or use one try catch, not one in each of the UDFs. Not only are things tidy, it forces you to create rigid code.
One thing to keep in mind here is that values passed from a form are _always_ strings. They are not typed. So a type setting of "string" on an argument that accepts values from a form is correct in this sense. "Any" will of course serve the same purpose.
As another poster mentioned, argument typing is meant to provide other programmers guidance as to what type of value is expected by a method in an API and "enforce" it so they don't have to dig behind the argument to figure that out. It's not designed to provide a validation / response mechanism to a user of an application.
We also can note that error trapping is a clumsy and resource intensive way of providing a simple response to a user, "Hey, we need a number here", or defaulting a value to 0 for instance if that's what is needed.
So, in the big picture, where would the preference for typing a variable be required (if at all) in the CF world?
The only place I can think of is when you're shoving data into a database/spreadsheet... Otherwise, doesn't it seem overkill to enforce type if CF is typeless?
I can see more use for validating against masks rather than atomic datatypes -- Credit card numbers, phone, email, etc... and those are best validated at user entry no matter which interface de jure you select.
Is that a truth that trapping an error costs more than pre-validating? Seems to me that you're spending the same time, just in different layers...
(Just purely out of curiosity, it would be interesting to know the design decision that prompted data typing on arguments...)
@Brian,
I think it depends on what type of application you are building. If you are providing an API that other developers (or you) will use, and you need to ensure that the developer passes in a struct and not a string for instance (or your code will break), then providing a type on the argument makes a lot of sense. The error that will be thrown to the developer will likely be more clear than the error you'd get from running the code that needs the struct (and got a string instead).
For validating form data, throwing errors is somewhat resource intensive, because CF's error handling machinery is put into play. Time a request that throws an error, catches it and returns a message to the user interface vs a request that processes an if statement and returns a message to the user interface.