Power Building with Exceptions


Link to this posting

Postby Ursego » 19 Feb 2013, 20:33

To pass errors from functions outwards, throw exceptions rather than return a success/error code (like 1/-1). That will result in shorter and better maintainable code.

From the book "Clean Code":

It might seem odd to have a section about error handling in a book about clean code. Error handling is just one of those things that we all have to do when we program. Input can be abnormal and devices can fail. In short, things can go wrong, and when they do, we as programmers are responsible for making sure that our code does what it needs to do. The connection to clean code, however, should be clear. Many code bases are completely dominated by error handling. When I say dominated, I don't mean that error handling is all that they do. I mean that it is nearly impossible to see what the code does because of all of the scattered error handling. Error handling is important, but if it obscures logic, it's wrong.

Use Exceptions Rather Than Return Codes

Back in the distant past there were many languages that didn't have exceptions. In those languages the techniques for handling and reporting errors were limited. You either set an error flag or returned an error code that the caller could check. The problem with these approaches is that they clutter the caller. The caller must check for errors immediately after the call. Unfortunately, it's easy to forget. For this reason it is better to throw an exception when you encounter an error. The calling code is cleaner. Its logic is not obscured by error handling.


To make a PB function to throw an exception, fill the "Throws:" field in the function's header (signature) with class Exception or its descendant.

When exceptions mechanism is in use, functions are called in the simplest possible way:

Code: Select all
uf_do_something()

As you see, there is no terrible code impurities like

Code: Select all
li_rc = uf_do_something()
if li_rc = -1 then return -1

or even

Code: Select all
if uf_do_something() = -1 then return -1

The tradition of returning a success/failure code 1/-1 came from the ancient times, when exceptions didn't exist in PowerBuilder yet. 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.

The next quote is taken from a Java book, but that principle works for any programming language:
In general, try to avoid return codes. Return codes are commonly used in searches, so programmers are expecting them. In other methods, you will take your callers by surprise by returning a special value. An exception forces the program to deal with them or end with the exception if left unhandled, whereas a return code could be accidentally ignored and cause problems later in the program. An exception is like shouting, "Deal with me!"


HOW TO DEAL WITH FUNCTIONS WHICH THROW EXCEPTIONS

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 ornament 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, another script (which calls script A) will bother head deciding what to do with the exception.

WHAT IS WRONG WITH THE BUILT-IN PB EXCEPTIONS? :cry:

It's easy 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 (it's guaranteed, that when developers will copypaste the fragment, which throws exception, they will forget to change the class and the script :lol: ).
3. The code, throwing the exception, must be compact - in fact, it's a piece of technical code, embedded into business logic, so it should be no longer than one line (like throw new Exception in Java and C#). So, the following solution is absolutely NOT acceptable:

Code: Select all
Exception    l_ex

[...code...]
if [problem 1] then
   l_ex = create Exception
   l_ex.SetMessage("[error message 1] in function uf_XXX of class n_YYY")
   throw l_ex
end if

[...code...]
if [problem 2] then
   l_ex = create Exception
   l_ex.SetMessage("[error message 2] in function uf_XXX of class n_YYY")
   throw l_ex
end if

Imagine: if many exceptions are thrown by the script (and hundreds, if not thousands, all over the application), and each time an exception object has to be created, populated and thrown... You will hardly see the business logic behind all that mess! That's why nobody uses exceptions in PowerBuilder. Hopefully, in a future version of PB, a one-line exception throwing command (like in Java and C#) will be added. But meanwhile I want to provide

A READY SOLUTION WHICH SOLVES ALL THESE PROBLEMS

In the suggested solution, three the actions with the exception (creation, population & throwing) are placed 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 in other languages):

Code: Select all
f_throw(PopulateError(0, "[error message]"))

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 the Error object. The numeric code, passed as the first argument to PopulateError(), helps the developer to find the problem spot when many exceptions are thrown in 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, supplied with the solution), populates it with the data, stored in the 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 (and, optionally, write into an errors log table or file, and even send email to a developer - but these additional services should be coded individually, n_ex only provides a placeholder for your custom code - the function uf_write_to_log() which does nothing by default). That's how all that is looking like:

Code: Select all
[...code...]
if [problem 1] then f_throw(PopulateError(1, "[error message 1]"))

[...code...]
if [problem 2] then f_throw(PopulateError(2, "[error message 2]"))

Do you see how shorter the code is? Now, nothing stops you from using exceptions all over the application - you have no excuse to "return -1" :lol: !

EXAMPLE OF USE:

Let's say, the following code fragment appears in function wf_show_classes_hierarchy() of the window w_spy:

Code: Select all
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() :

Image

HOW TO ADD THE SOLUTION TO THE APPLICATION?

1. Save the file spy.pbl on your hard disk (in the folder where the PBLs are stored).
2. Add it to your application's library list.

That PBL contains a few objects, but we need only n_ex and f_throw for the exceptions functionality.

THERE ARE TWO KINDS OF EXCEPTIONS - TECHNICAL AND BUSINESS EXCEPTIONS

Technical exceptions:

They should never happen, i.e. they are bugs which require a fix.

Examples of situations:
@ An empty mandatory parameter passed to a function.
@ A choose case construction has no case for a customer status, added to the application recently (see Defensive programming. Defuse time bombs!)
@ No rows retrieved into a DataStore when at least one row is required (probably, a problem in the WHERE clause).

When you use exceptions to handle technical errors in multi-levels, multi-branches calls hierarchies, the rule is simple:

Handle exceptions only when there is absolutely no other choice. 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), or in top-level scripts (which start the calling chains) of your application's framework (if you use a shelf product like PFC, then you are not allowed to change its functions headers). Non-framework scripts should not have their own try...catch blocks - they should always pass technical 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.

Business exceptions:

They are not bugs and absolutely can happen during the normal execution of the application - in fact, they are the means to branch execution flow.

Examples of situations:
@ User tries to archive an order which has not been paid.
@ User clicks OK button when no row in a DW is selected.
@ User closes a window with unsaved changes.

When you use exceptions to handle business-related errors, exceptions of that kind CAN be caught in not-root-level scripts. I.e. they are exempted from the rule which forces us to handle exceptions only when there is absolutely no other choice (that rule is applicable only to technical exceptions). In fact, the purpose of business exceptions is to inform calling scripts about special business situations, so they should be caught and handled in those calling scripts (otherwise, why to throw them? :lol: ). In general, the proposed f_throw is not supposed to be used for business-related exceptions, but you still can use it if you utilize the exception's error message to transport the information about the problem.

Here is an example of a fragment which throws a business exception. Let's say, it is coded in the function uf_process_customers. In addition to this business exception, the function can throw technical exceptions as well (I will show later how to distinguish between them in the calling script). Messages, serving business exceptions, must begin with a conventional sequence of characters (like "###" in the next example). The convention is to use that sequence only in the beginning of the error messages for business exceptions; technical exceptions are absolutely prohibited from starting with them!

Code: Select all
if dw_cust.GetSelectedRow(0) = 0 then
   f_throw(PopulateError(0,"### NO CUST SELECTED"))
end if

The fragment of the calling script which handles the exception:

Code: Select all
try
   uf_process_customers()
catch(n_ex e)
   if e.GetMessage() = "### NO CUST SELECTED" then
      MessageBox("No customers selected", "Please select at least one customer.")
   else
      f_throw(PopulateError(0, e.GetMessage())) // it's a technical exception - re-throw it
   end if
end try

Pay attention that the technical exceptions are not handled but passed outwards. That is very dangerous because the caught exception can be forgotten to be re-thrown. That will interrupt the current (cut the chain of exception propagation) and produce a hidden bug. So, it's a bad idea to use the proposed mechanism for business exceptions. It's better to utilize the old good return codes to report abnormal business situations, and use exceptions only for bugs.

That is not the best solution for business exceptions by one more reason - it is bug-prone since the error messages are hard-coded: the text can be misspelled or changed in one of the functions. Ideally, the caught exceptions must be distinguished by the type (class) of the exception object, not by the text of the error message - that's what we enjoy in Java, .Net and PL/SQL. But the situation in PB can be improved if you use string constants rather than rely on hard-coded texts. The constants can be declared in n_ex, for example:

Code: Select all
public constant string NO_CUST_SELECTED = "### NO CUST SELECTED"

So, instead of

Code: Select all
f_throw(PopulateError(0,"### NO CUST SELECTED"))
...
if e.GetMessage() = "### NO CUST SELECTED" then

you will write

Code: Select all
f_throw(PopulateError(0, n_ex.NO_CUST_SELECTED))
...
if e.GetMessage() = e.NO_CUST_SELECTED then


IF THE FUNCTION IS POSTED

Exceptions are irrelevant 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 will be thrown later, after script A has finished running. So, be careful - you can interrupt the current unintentionally!

If you write a function which is supposed to be POSTed, then:

1. Put all its code in a try...catch block and display the error message in the catch, i.e. inside the function. Unfortunately, the calls chain will not be interrupted, but the error message will be shown – that is the most importantly, we don't want hidden bugs.

2. Re-throw the exception from within the catch, just after displaying the error message - to interrupt the calls chain if the function will be TRIGGERed (rather than POSTed) in the future. That will result in double error message, but it's not a huge trouble.

If you want to 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 its try...catch, and POST that wrapper function.
User avatar
Ursego
Site Admin
 
Posts: 130
Joined: 19 Feb 2013, 20:33

Link to this posting

Postby Ursego » 20 Nov 2013, 11:24

PRE-COOKED CODE FRAGMENTS

I have collected a few ready code fragments useful in some standard "exceptional" :lol: situations. You can save them in a file and and use when needed with some customization:

Code: Select all
f_throw(PopulateError(0, ""))
if IsNull(ls_xxx) then f_throw(PopulateError(0, "ls_xxx is NULL."))
if IfNull(ls_xxx, '') = '' then f_throw(PopulateError(0, "ls_xxx is empty.")) // IfNull(): http://code.intfast.ca/viewtopic.php?t=5
if li_rc <> 1 then f_throw(PopulateError(0, "uf_xxx failed. Arguments: ..., ..."))
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()) + "."))
if ll_row_count <> 1 then f_throw(PopulateError(0, "Row count must be 1, not " + String(ll_row_count) + "."))
f_throw(PopulateError(0, "Argument as_mode contains illegal value " + IfNull("'" + as_mode + "'", "NULL") + ".")
f_throw(PopulateError(0, "Argument ai_mode contains illegal value '" + IfNull(String(ai_mode), "NULL") + "'.")
f_throw(PopulateError(0, "This function MUST never be called. It MUST be implemented in the descendant class " + this.ClassName() + "."))

Some defensive programming:

Code: Select all
choose case as_xxx
case "aaa"
   // do something
case "bbb"
   // do something else
case else
   f_throw(PopulateError(0, "Argument as_xxx contains illegal value " + IfNull("'" + as_xxx + "'", "NULL") + ". It must be 'aaa' or 'bbb'."))
end choose

The try...catch block:

Code: Select all
try
   
catch(n_ex e)
   e.uf_msg()
end try
User avatar
Ursego
Site Admin
 
Posts: 130
Joined: 19 Feb 2013, 20:33

Link to this posting

Postby Ursego » 04 Jul 2019, 07:43

From the book "97 Things Every Programmer Should Know":

Distinguish Business Exceptions from Technical

There are bas ically two reasons that things go wrong at runtime: technical problems that prevent us from using the application and business logic that prevents us from misusing the application. Most modern languages, such as LISP, Java, Smalltalk, and C#, use exceptions to signal both these situations. However, the two situations are so different that they should be carefully held apart. It is a potential source of confusion to represent them both using the same exception hierarchy, not to mention the same exception class.

An unresolvable technical problem can occur when there is a programming error. For example, if you try to access element 83 from an array of size 17, then the program is clearly off track, and some exception should result. The subtler version is calling some library code with inappropriate arguments, causing the same situation on the inside of the library.

It would be a mistake to attempt to resolve these situations you caused yourself. Instead, we let the exception bubble up to the highest architectural level and let some general exception-handling mechanism do what it can to ensure that the system is in a safe state, such as rolling back a transaction, logging and alerting administration, and reporting back (politely) to the user.

A variant of this situation is when you are in the "library situation" and a caller has broken the contract of your method, e.g., passing a totally bizarre argument or not having a dependent object set up properly. This is on a par with accessing the 83rd element from 17: the caller should have checked; not doing so is a programmer error on the client side. The proper response is to throw a technical exception.

A different, but still technical, situation is when the program cannot proceed because of a problem in the execution environment, such as an unresponsive database. In this situation, you must assume that the infrastructure did what it could to resolve the issue - repairing connections and retrying a reasonable number of times - and failed. Even if the cause is different, the situation for the calling code is similar: there is little it can do about it. So, we signal the situation through an exception that we let bubble up to the general exception-handling mechanism.

In contrast to these, we have the situation where you cannot complete the call for a domain-logical reason. In this case, we have encountered a situation that is an exception, i.e., unusual and undesirable, but not bizarre or programmatically in error (for example, if I try to withdraw money from an account with insufficient funds). In other words, this kind of situation is a part of the contract, and throwing an exception is just an alternative return path that is part of the model and that the client should be aware of and be prepared to handle. For these situations, it is appropriate to create a specific exception or a separate exception hierarchy so that the client can handle the situation on its own terms.

Mixing technical exceptions and business exceptions in the same hierarchy blurs the distinction and confuses the caller about what the method contract is, what conditions it is required to ensure before calling, and what situations it is supposed to handle. Separating the cases gives clarity and increases the chances that technical exceptions will be handled by some application framework, while the business domain exceptions actually are considered and handled by the client code.
User avatar
Ursego
Site Admin
 
Posts: 130
Joined: 19 Feb 2013, 20:33




IF you want to ((lose weight) OR (have unbelievable (brain function AND mental clarity))) THEN click:




cron
free counters

eXTReMe Tracker