The tradition of returning a success/failure code 1/-1 came from the ancient times, when exceptions didn't exist yet in PowerBuilder. But there is no need to use horses in the automobiles era! We still check codes, returned by existing functions (if they return it), but be a modern developer writing new code!
HOW TO DEAL WITH FUNCTIONS WHICH THROW ECXEPTIONS
The rule is simple: if script A calls script B and script B throws an exception, then script A has two, and only two, choices, forced by the compiler:
1. To process (i.e. to catch) the exception. For that, script A must surround calling script B with a try...catch block. 2. Not to process the exception (i.e. to pass it outwards by filling the field "Throws:" in the header). In that case, an outer script, calling script A, will bother head deciding what to do with the exception.
IF THE FUNCTION IS POSTED
The just described rule doesn’t work if the function is POSTed. If script A calls script B with POST and script B throws an exception, then the compiler doesn’t force script A to handle that exception. The reason is obvious: in the runtime, script A can do nothing at that point of time - the exception is thrown after script A has been finished running. So, be careful - you can interrupt the current unintentionally! If you write a function which is supposed to be POSTed, then:
1. Take its whole logic into a try...catch block (so, the error message will be shown – even thou the calls chain will not be interrupted). 2. Re-throw the exception from within the catch - to interrupt the calls chain in case of TRIGGERing function in the future (that will result in double error message but it’s not a big deal).
If you POST an existing function which throws an exception, take its whole logic into a try...catch and re-throw the exception in the same way. If that is impossible (or you don’t want to change a function of another developer), write a wrapper function which only calls that function ( TRIGGERs, not POSTs!) inside of try...catch.
WHAT IS WRONG WITH THE BUILT-IN PB EXCEPTIONS?
It's not a big deal to throw an exception in PB 8 or later, but, IMHO, 3 very important conditions must be taken into account:
1. The error message, describing the problem, should display the class and the script where the problem occurred. 2. That info should be populated automatically rather than typed by the developer each time manually. 3. The code, throwing the exception, must be compact - in fact, it's a piece of technical code, imbedded into business code, so it should be no longer than one line (like "throw new Exception" in C#). Imagine: if your script throws many exceptions, and each time an exception object has to be created, populated and thrown... You will hardly see the business logic behind all that technical garbage!
So, the following solution is absolutely NOT acceptable:
try [...code...] if [condition of Problem 1] then l_ex = create Exception l_ex.SetMessage("[description of Problem 1] in function uf_XXX of class n_YYY") throw l_ex end if
[...code...] if [condition of Problem 2] then l_ex = create Exception l_ex.SetMessage("[description of Problem 2] in function uf_XXX of class n_YYY") throw l_ex end if catch(Exception e) MessageBox("Error", e.GetMessage()) end try
HOW TO SOLVE ALL THOSE PROBLEMS AT ONE STROKE?
In the suggested solution, three the actions with the exception (creation + population + throwing) are put in one function named f_throw() (make it a public method of an NVO if you wish, but I prefer to have it as a global function even though I am against them - in fact, it plays the role of the throw keyword):
As you see, all the described conditions are satisfied. PopulateError() (called inside the argument parentheses of f_throw()) grabs the needed details of the thrown exception (class, script and even line number) and stores them in Error object. The numeric code, passed as the first argument to PopulateError(), helps the developer to find the problem spot when a few exceptions are thrown from a same script; simply pass 0 if you don't need that. The function f_throw()creates an instance of Exception (more exactly - its descendant n_ex, later I will show how to create it), populates it with the data, stored in Error object, and re-throws. The function uf_msg() (which must be called from within the exception handler section) "knows" how to use all that data to build a nice error message.
try [...code...] if [condition of Problem 1] then f_throw(PopulateError(1, "[description of Problem 1]"))
[...code...] if [condition of Problem 2] then f_throw(PopulateError(2, "[description of Problem 2]")) catch(n_ex e) e.uf_msg() end try
Do you see how shorter that simple example is? And if your script throws a lot of exceptions? And hundreds, if not thousands, all over the application?
In the last example the exception is thrown and caught in a same script. It's not a common practice - usually exceptions are thrown and caught in different methods (i.e. propagate through methods calls). In that case, don't forget to populate the field "Throws:" in the called function's header with n_ex!
EXAMPLE OF USE:
Let's say, the following code fragment appears in function wf_show_classes_hierarchy() of the window w_spy:
try f_throw(PopulateError(3, "Something terrible happened!")) catch(n_ex e) e.uf_msg() end try
Here is the message, displayed by e.uf_msg() :
HOW TO ADD THE DESCRIBED MECHANISM TO THE APPLICATION?
1. Save the files n_ex.sru and f_throw.srf on your hard disk (you can click on them to se the code if it's interesting how they work). 2. Import them into your application (n_ex.sru firstly, then f_throw.srf!).
THE ART OF EXCEPTIONS PROPAGATION
We can throw exceptions of two kinds - technical and business.
Examples of technical errors: empty mandatory parameter passed to a function; choose case cannot process a new status; no rows retrieved into a DataStore when at least one row had to be retrieved.
Examples of business errors: user ties to increase a salary in more than 10%; user clicks OK button when no row in a DW is selected.
When you use exceptions to handle technical errors in multi-levels, multi-branches calls hierarchies, the rule is simple: the try...catch block should appear only in the root-level scripts, i.e. in the built-in PowerBuilder events like Clicked or ItemChanged (where we physically cannot pass exceptions out). All the scripts, created by developers, i.e. all your uf_XXX-s and ue_XXX-s (called from within the root level in any depth of the calling hierarchy), should not have their own try...catch blocks - they only transfer exceptions outwards.
If you develop a component to be consumed by other parts of the application (like a controller NVO) then don't catch exceptions inside of it at all - even in its public functions, called from outside. Those public functions are the root level only for the NVO, but not per the whole app. So, the rule is simple: pass technical exceptions out whenever you can do that physically.
But when you use exceptions to handle business-related errors, exceptions of that kind CAN be caught in not-root-level scripts. In fact, their purpose is to inform calling scripts about special business situations, so they should be caught and handled in those scripts.
Here is an example (please pay attention that n_ex is not caught but forwarded out):
try // Next func throws n_ex (for technical issues), n_ex_salary_increase_too_large, n_ex_no_access_in_week_ends: uf_increase_salary(adec_increase_amt) catch(n_ex_salary_increase_too_large le_sal) MessageBox("Ooops...", "You cannot increase salary so dramatically!") catch(n_ex_no_access_in_week_ends le_no_acc) MessageBox("Ooops...", "Be with your family, it's not a working hour!") end try
f_throw(PopulateError(0, "")) if IsNull(ls_xxx) then f_throw(PopulateError(0, "ls_xxx is NULL.")) if IsNull(ls_xxx) or Trim(ls_xxx) = '' then f_throw(PopulateError(0, "ls_xxx is empty.")) if ll_row_count <> 1 then f_throw(PopulateError(0, "Row count must be 1, not " + String(ll_row_count) + ".")) if li_rc <> 1 then f_throw(PopulateError(0, "uf_xxx failed.")) if not IsValid(ads_xxx) then f_throw(PopulateError(0, "ads_xxx is invalid.")) if ads_XXX.RowCount() < 1 then f_throw(PopulateError(0, "ads_XXX has no rows.")) if ads_XXX.RowCount() <> 1 then f_throw(PopulateError(0, "ads_XXX.RowCount() must be 1, not " + String(ids_XXX.RowCount()) + ".")) f_throw(PopulateError(0, "This function MUST never be called. It MUST be implemented in descendant object " + this.ClassName() + "."))