Best Practices for using Exceptions in Uniface
Uniface exceptions change the way you deal with ProcScript errors caused by abnormal runtime events. Writing code in Uniface that is exception enabled is different from other programming languages, and there are some pitfalls to avoid.
Here are some best practices to help you write robust code that is error safe:
Exceptions are not automatically enabled in Uniface. To allow a module to throw an exception, add a throws declaration directly below the module declaration.
By enabling exceptions, every $procerror causes an exception which terminates the program if it is not caught.
Good code practice: | Bad code practice: |
---|---|
entry myModule throws ; This code causes an exception because the entity does not exist clear/e "NON_EXISTING_ENTITY" end |
entry myModule ; no throws ; This time, the error is ignored clear/e "NON_EXISTING_ENTITY" end |
Only explicitly catch exceptions that are expected using try-catch
blocks. It is good practice to let all other exceptions bubble up, allowing them to exit the application as an uncaught exception that indicates a problem that needs fixing. This will help you avoid bugs going unnoticed.
Good code practice: | Bad code practice: |
---|---|
try retrieve catch -2; you expect that no data is available putmess "No data found" endtry |
try retrieve catch ; catches all exceptions putmess "No data found" endtry |
Limit the amount of code inside the try block to only the instruction you expect may throw an exception you want to catch. This prevents suppressing unexpected exceptions that may indicate a real problem. This makes the code clearly readable, to show in the example below that the -2
error is only expected and caught for retrieve.
Good code practice: | Bad code practice: |
---|---|
entry myRetrieveBoth throws clear/e "ORDER" KEY.ORDER/init = $profile$ retrieve/e "ORDER" clear/e "ORDERLINE" KEYORDER.ORDERLINE/init = KEY.ORDER try retrieve/e "ORDERLINE" catch -2 ; No data for ORDERLINE is ignored endtry end |
entry myRetrieveBoth throws try clear/e "ORDER" KEY.ORDER/init = $profile$ retrieve/e "ORDER" clear/e "ORDERLINE" KEYORDER.ORDERLINE/init = KEY.ORDER retrieve/e "ORDERLINE" catch -2 ; If ORDERLINE has no data, this is also ignored endtry end |
Use the standard Uniface validation functionality to deal with the validation of field data. Exceptions are not intended to be used for validation, because data validation errors are not coding errors, nor are they caused by environment or configuration problems. Data validation errors are caused when the end user enters incorrect data into fields, as determined by declarative and procedural validation rules.
Throwing an exception from a validate trigger to show the user they have entered incorrect data typically stops further code execution and the application cannot easily get back into a normal flow. Instead, keep using error triggers, as they fire automatically when a data validation error occurs.
You should still add a throws declaration in a validation trigger. This allows any other type of error, such as a coding mistake, to cause an exception, so that you can identify and fix it.
Good code practice: | Bad code practice: |
---|---|
trigger validate throws if !(STARTDATE < ENDDATE) ; Return a negative value to fire the error trigger return -1 endif end ; Use the error trigger to report any type of data validation error trigger error throws if ($error == 0153) ; Error caused by validate trigger if ($item("STATUS", $dataerrorcontext) == -1) message "STARTDATE must be before ENDDATE" endif endif end |
trigger validate throws if ! (STARTDATE < ENDDATE) throw -1, "STARTDATE must be before ENDDATE" endif end trigger error throws ; This trigger is not fired because the validate trigger ; threw an exception instead of returning a negative value. end |
Split code with specific exception handling off into separate modules to keep the main code clean and readable. This also allows easier reuse and better isolation of concerns.
Good code practice: | Bad code practice: |
---|---|
; Clean main module entry loadOrder throws params string pOrderTechKey: in endparams call getOrderByTechKey(pOrderTechKey) ; Data is expected call getOrderlineByOrder(pOrderTechKey) ; Data is not expected end entry getOrderByTechKey throws params string pOrderTechKey: in endparams clear/e "ORDER" KEY.ORDER/init = pOrderTechKey retrieve/e "ORDER" ; <UIOSERR_OCC_NOT_FOUND> (-2) and <UIOSERR_OPEN_FAILURE> (-4) will throw an exception end entry getOrderlineByOrder throws params string pOrderTechKey: in endparams clear/e "ORDERLINE" KEYORDER.ORDERLINE/init = pOrderTechKey try retrieve/e "ORDERLINE" catch <UIOSERR_OCC_NOT_FOUND>, <UIOSERR_OPEN_FAILURE> ; -2, -4 ; No data or table endtry end |
; Works but is a bit less readable entry loadOrder throws params string pOrderTechKey: in endparams clear/e "ORDER" KEY.ORDER/init = pOrderTechKey retrieve/e "ORDER" clear/e "ORDERLINE" KEYORDER.ORDERLINE/init = pOrderTechKey try retrieve/e "ORDERLINE" catch <UIOSERR_OCC_NOT_FOUND>, <UIOSERR_OPEN_FAILURE> ; -2, -4 ; No data for ORDERLINE endtry end |
When getting an item from a list, commonly the item is allowed to not exist. When using $item() or $itemnr(), catch the exception -1129
<UPROCERR_ITEM> if the item is allowed to not exist. If you do not catch this error, it is considered a fatal error by the application.
Alternatively, use the statement counterparts getitem/id or getitem. These statements do not throw an exception, but instead set $status to indicate whether the item was found or not.
Good code practice: | Bad code practice: |
---|---|
entry getItem throws getitem/id ITEM, LIST, ("can_exist") if ($status > 0) ; Item exists else ; Item does not exist ITEM = "" endif end |
entry getItem throws ; This throws an exception if the item does not exist ITEM = $item("can_exist", LIST) if (ITEM == "") ; We do NOT reach this point if the item does not exist ; Instead, we get an exception. ; We DO reach this point if the value of the item is "". endif end
|
entry getItem throws try ITEM = $item("can_exist", LIST) catch <UPROCERR_ITEM> ; -1129 ; Item does not exist ITEM = "" endtry end |
To iterate through occurrences of an entity, use the forentity statement instead of a while loop construct with a setocc statement, where possible. The setocc statement throws an exception -1203 <UPROCERR_RANGE> when it tries to set the focus on a value out of range, in other words, the last occurrence plus one. This exception causes the module to exit, rather than exiting the while loop.
If setocc is needed, isolate it in a helper function and deal with the specific exception there.
Good code practice: | Bad code practice: |
---|---|
entry loopCollection throws forentity "MYENT" ; Do something with the occurrence endfor end |
entry loopCollection throws setocc "MYENT", 1 while ($status > 0) ; Do something with the occurrence setocc "MYENT", $curocc(MYENT) + 1 endwhile ; We never reach this point, because setocc will throw ; an exception -1203 on the last+1 occurrence and exit the module. end
|
entry loopCollection throws call mySetocc("MYENT", 1) while ($status > 0) ; Do something with the occurrence call mySetocc("MYENT", $curocc(MYENT) + 1) endwhile end entry mySetocc throws params string pEntityName: in numeric pOccurrenceIndex: in endparams try setocc pEntityName, pOccurrenceIndex return $status catch <UPROCERR_RANGE> ; -1203 return 0 endcatch end |
To catch exceptions thrown by interactive triggers while a modal form is waiting for user input and Uniface is therefore in idle mode, put a try-catch block around the edit statement in the exec operation of the modal form. The edit statement starts the idle mode and is therefore considered the call stack parent of idle mode for a modal form.
Catch only specific exceptions and let all other exceptions bubble through. The edit statement is the parent of all interactive triggers of modal forms and all exceptions bubble through it.
Good code practice: | Bad code practice: |
---|---|
; cpt:MYMODALFORM operation exec throws try edit catch -10001 ; Catch specific exceptions thrown by ; interactive triggers of a modal form here... endtry end trigger detail throws throw -10001, "My custom exception" end |
; cpt:MYMODALFORM operation exec throws try edit catch ; Do not catch generic exceptions endtry end trigger detail throws throw -10001, "My custom exception" end |
To catch exceptions thrown by interactive triggers while a non-modal form is waiting for user input and Uniface is therefore in idle mode, put a try-catch block around the apstart statement in the apStart trigger of the startup shell. The apstart statement starts the idle mode and is therefore considered the call stack parent of idle mode for a non-modal form.
Catch only specific exceptions and let all other exceptions bubble through. The apstart statement is the parent of the interactive triggers of non-modal forms and many exceptions bubble through it.
Good code practice: | Bad code practice: |
---|---|
; aps:MYAPS trigger apstart activate "MYFORM1".exec() try apstart catch -10001 ; Catch specific exceptions thrown by ; interactive triggers of a non-modal form here... endtry end ; cpt:MYNONMODALFORM operation exec throws edit end ; cpt:MYNONMODALFORM/ent:MYENT.MODEL/fld:MYBUTTON trigger detail throws throw -10001, "My custom exception" end |
; aps:MYAPS trigger apstart try activate "MYFORM1".exec() catch -10001 ; Exceptions from interactive triggers ; of a non-modal form do not end up here! endtry end ; cpt:MYNONMODALFORM operation exec throws try edit catch -10001 ; Exceptions from interactive triggers ; of a non-modal form do not end up here! endtry end ; cpt:MYNONMODALFORM/ent:MYENT.MODEL/fld:MYBUTTON trigger detail throws throw -10001, "My custom exception" end |
To catch exceptions thrown by interactive DSP triggers, like detail, put a try-catch around the activate statement in the collection operation activateRequest of cpt:USYSHTTP/ent:DSP.HANDLER. This activate statement fires any DSP triggers.
Catch only specific exceptions and let all other exceptions bubble through. The activate statement is the parent of all DSP triggers of the web application and many exceptions bubble through it.
Good code practice: | Bad code practice: |
---|---|
; cpt:USYSHTTP/ent:DSP.HANDLER partner operation activateRequest throws ... selectcase vActivateType case "DSPtrigger" ; This is a DSP trigger request, ; the kernel will map this activate to the trigger invocation try activate "%%(vInstanceName)" vActivateStatus = $status catch -10001 ; Catch specific exceptions ; thrown by interactive DSP triggers here... endtry ... endselectcase ... end ; cpt:MYDSP/ent:MYENT.MODEL/fld:MYBUTTON trigger detail throws public web throw -10001, "My custom exception" end |
; cpt:MYDSP operation exec throws public web try edit catch -10001 ; Exception from interactive triggers ; of a DSP do not end up here! endtry end ; cpt:MYDSP/ent:MYENT.MODEL/fld:MYBUTTON trigger detail throws public web throw -10001, "My custom exception" end |
To catch exceptions thrown by interactive USP triggers, like detail, put a try-catch around the webget (or edit) statement in the exec operation of that USP. The webget statement fires any USP triggers.
Catch only specific exceptions and let all other exceptions bubble through. The webget statement is the parent of all USP triggers of that component and many exceptions bubble through it.
Good code practice: | Bad code practice: |
---|---|
; cpt:MYUSP operation exec throws public web try webget ; or edit catch -10001 ; Catch specific exceptions ; thrown by interactive USP triggers here... endtry end ; cpt:MYUSP/ent:MYENT.MODEL/fld:MYBUTTON trigger detail throws throw -10001, "My custom exception" end |
; cpt:MYUSP operation exec throws public web try edit catch ; Do not catch generic exceptions... endtry end ; cpt:MYUSP/ent:MYENT.MODEL/fld:MYBUTTON trigger detail throws throw -10001, "My custom exception" end |
In a production environments, log uncaught exceptions instead of reporting them to the end user, because the details about the exception might show implementation details that could be abused.
Good code practice: | Bad code practice: |
---|---|
;aps:MYAPS trigger apstart throws #ifdefined DEVELOPMENT ; Show uncaught exception on the screen activate "MYINST".calculate ($1, $2) #else ; Log uncaught exceptions and show generic error on the screen try activate "MYINST".calculate($1, $2) catch vExceptionId = logException($procerrorcontext) throw -1, "Your program has terminated", "EXCEPTIONID=%%(vExceptionId)" endtry #endif end |
; aps:MYAPS trigger apstart throws ; All exception details are shown activate "MYINST".calculate($1, $2) end |
Place module related cleanup logic, such as data cleanup or closing a connection, in the finally block of a try-endtry block. This ensures that cleanup activities are performed irrespective of whether an exception occurs or not, and allows you to write this code in just one place in the module for more readable and maintainable code.
When using self destructing statements that delete the current instance, such as deleteinstance <$instancename>, exit, or apexit, inside a try block of a try-finally-endtry construct, further code execution stops because the current runtime context has been deleted, and the finally block is not executed.
It is best practice to delete an instance from outside the same instance, to ensure all context stays available while handling any exceptions. If deleting the current instance cannot be avoided, place any module-related cleanup logic just before the deleteinstance<$instancename> or exit statement, and not in the finally block.