Ask Reuben
When Things Go Wrong (part 3 of 3) – Exception Handling
My program has stopped, how can I tell what has gone wrong?
How can I handle errors gracefully whilst as the same time capturing as much information as possible?
In last weeks article, the code example we used attempted a divide by zero and wrote a simple error to a log file. This is an example of an exception, and what I will look at today is what we can do when we encounter an exception.
An exception is an “abnormal runtime event that can be trapped for control“. First thing to make clear is not all things that go wrong can be trapped for control. This section of the documentation explains a little more about these untrappable errors and what happens should they occur. In general, these indicate problems with the UI such as the –1110 error which means it is not safe for the UI to continue running, or problems with the stack or memory such as the –1320 error that means it is not safe for the fglrun process to continue running, and it is accepted that the program should do as little as possible (in this case writing 4 lines to the errorlog, and outputting a small dialog box) before stopping.
For exceptions where it is safe to continue, the WHENEVER instruction and the newer TRY/CATCH syntax allow us to stop, carry on, or exit more gracefully.
Strictly speaking, the WHENEVER instruction is a compile-time directive. For the lines beneath it physically in the .4gl file, it instructs the compiler to execute a certain action. So for a contrived example like this …
WHENEVER ANY ERROR CONTINUE FOR i = 1 TO 0 STEP -1 DISPLAY 1 / i WHENEVER ANY ERROR STOP END FOR
… the exception handler for the DISPLAY 1/i line is always to CONTINUE. It is not the case that on the second iteration of the line that the exception handler has changed to STOP. Similar this type of code …
IF continue_on_error THEN WHENEVER ANY ERROR CONTINUE ELSE WHENEVER ANY ERROR STOP END IF DISPLAY 1/0
… it does not matter what the value of the variable continue_on_error is, the exception handler in place at the time of the DISPLAY 1/0 is to STOP.
As it is a compile time directive, what I like is to set up a code standard that the very first function of a .4gl is a private function that has this line to set the exception handler in place for all the lines of code in the 4gl file.
#! The first function at the top of every 4gl in my system PRIVATE FUNCTION exception_handler() WHENEVER ANY ERROR CALL my_error_handler END FUNCTION
If there is an exception, then the function my_error_handler() will be executed. The only .4gl you can’t do this is for a .4gl that contains the MAIN block. You are not allowed a FUNCTION before MAIN so in that .4gl the WHENEVER ANY ERROR line has to be one of the first lines of the MAIN.
If I don’t want an exception to call this function, maybe because you want to explicitly handle the exception yourself in your code and typically then carry on, then you can surround the lines you want to be handled by yourself in a TRY/CATCH block e.g.
TRY DISPLAY a/b CATCH RETURN FALSE, "b cannot be zero" END TRY RETURN TRUE, ""
If an exception occurs inside the TRY block, then the runner will execute the code in the CATCH and carry on.
The end result I am going for is that if any exception occurs in my code, my function my_error_handler is called unless I have specifically handled the exception using a TRY/CATCH block.
It is in this function that I then handle the exception gracefully. In this function you can display some nice UI, send an email to a system administrator, rollback and close database connections etc before stopping the program. For the whowhatwhen table I introduced 2 ask-reubens ago, this is where you can update the table to reflect the fact that the program stopped with an exception. I have seen customers read the last 4 lines of the errorlog so that they can display some information about the error to the end-user.
You just have to be careful not to do too much as you have to consider what happens if the code inside this function has an exception. You don’t want to end up in an infinite loop because the code handling the exception has an exception itself.
As an example, try the following …
MAIN -- Compile time directive of what to do if exception occurs on lines below WHENEVER ANY ERROR CALL my_error_handler MENU "Run program" ON ACTION run EXIT MENU END MENU CALL STARTLOG("askreuben16.log") CALL ERRORLOG("Program started") -- Deliberate exception DISPLAY 1/0 END MAIN #! called whenever an unhandled exception occurs FUNCTION my_error_handler() -- compile time directive to avoid endless loop if my_error_handler has an error WHENEVER ANY ERROR STOP -- Add code here to handle error gracefully -- Display nice instructional dialog MENU "Error" ATTRIBUTES(STYLE="dialog", IMAGE="stop", COMMENT="Something bad has happend.\nPlease contact your system administrator") ON ACTION accept EXIT MENU END MENU -- Terminate program EXIT PROGRAM 1 END FUNCTION
… if you run this, you should observe that a “nice” dialog appeared where you have control of the message. If you want to see examples of why you should have a nice UI to control the dialog, replace the WHENEVER ANY ERROR CALL my_error_handler in line 3 with WHENEVER ANY ERROR STOP. In recent versions of Genero you will get a system generated dialog as the program stops. Earlier versions of Genero your program would stop with no UI, the user would be left wondering what has happened to their program. If you leave the WHENEVER ANY ERROR STOP and comment out the MENU statement so that no UI has been generated, you will also see the program stop with no UI. You will see something written to stderr but in a GDC or GBC program, the user cannot see stderr.
There is more to exception handling which I won’t cover here. I will make two points for you to read on your own.
Note the various exception-classes and the meaning of the keyword ANY.
Note the various exception-actions. For 3rd party libraries consider writing them with WHENEVER ANY ERROR RAISE. This will pass responsibility for the exception handling back to the calling function.
So to summarise this three part series :-
- consider introducing a whowhatwhen table or equivalent to audit what programs are started and by who, and wether they exit without error or not.
- use the errorlog and use a sensible file name structure so that content of the errorlog can easily be read when analysing an issue.
- handle any exceptions yourself in your code by using WHENEVER ANY ERROR CALL function-name, so that any exceptions pass through a common function that tidies up, records the exception, and has some tidy UI for the end-user.
You should then find that users perceive your application as more professional although despite the fact it has crashed with an exception, it has done so in a tidy and professional manner. You also have more information upon which to analyse the error.