The most valuable new feature in APEX 4.1 is definitely the possibility to implement an error handling function. It basically allows you to execute a PL/SQL function every time an error occurs in your application. An error handling function accepts one parameter of type apex_error.t_error which holds all relevant error information. The apex_error.t_error_result type is used as the return type and contains information on how the error message should be presented to the end user.
I won’t get into too much detail on how an error handling function should look like since all of this is well-documented. Then there is also Patrick Wolf who wrote a great article regarding the why and how of error handling.
A good starting point to build your own function is the template code which can be found here. The purpose of this article is to provide and explain a hands-on example of an error handling function that allows you to take advantage of following possibilities:
- Error logging
- Define an error message its display location
- Detect severe internal exceptions and modify the error message
- Map a constraint violation to a specific error message
- E-mail notification when an unexpected error occurs
Getting started
The error handling function is being executed for all sorts of errors. I make a distinction between expected and unexpected errors. The occurrence of an unexpected error is vital information for a developer since it reveals serious shortcomings of the application logic or validations. I opted to only log this kind of errors, but nothing stops you from logging expected errors as well.
You first need a table to hold all logged errors. My ERROR_LOG table contains all the information on the encountered error (retrieved from the incoming parameter of type t_error), plus some general information derived from substitution strings, and a time stamp.
P.S. I included the ID of the application in which the error occurred because I make use of one centralized error handling function for multiple applications. This can be easily achieved by creating private synonyms that point to the database object in which the error handling function resides.
Now let’s dig in some code. We start with the header section and first part of the error handling function:
FUNCTION fun_catch_error(p_error IN apex_error.t_error) RETURN apex_error.t_error_result AS l_result apex_error.t_error_result; l_reference_id error_log.error_id%TYPE; l_constraint_name constraint_lookup.constraint_name%TYPE; BEGIN l_result := apex_error.init_error_result(p_error => p_error); ...
The function header simply accepts the error as a parameter and returns the t_error_result type. Some comments on the variables we’re using:
- l_result is our return variable
- l_reference_id will be assigned a primary key generated on an insert of the ERROR_LOG table
- l_constraint_name will be assigned the name of the violated constraint, if applicable
In the first line of code we initialize the l_result variable with the values stored in our incoming parameter p_error. For example, if an end user violates a unique constraint then l_result.message will be initialized by a value that looks like this: “ORA-00001: unique constraint (SCHEMA.CONSTRAINT_NAME) violated”.
Logging an error
I wrote a private function fun_log_error to perform an insert on the ERROR_LOG table. Please note that this function must be defined as an autonomous transaction. An autonomous transaction allows you to leave the context of the calling transaction, in this case our error handling function, so you can start a new independent transaction to perform the insert. This is absolutely mandatory since the calling transaction will always be rolled back every time we hit an unexpected error.
FUNCTION fun_log_error(p_error IN apex_error.t_error) RETURN error_log.error_id%TYPE AS PRAGMA AUTONOMOUS_TRANSACTION; l_error_id error_log.error_id%TYPE; l_is_internal_error error_log.is_internal_error%TYPE; BEGIN IF p_error.is_internal_error THEN l_is_internal_error := 'Y'; ELSE l_is_internal_error := 'N'; END IF; -- INSERT INTO error_log (message, additional_info, display_location, association_type , page_item_name, region_id, column_alias, row_num , is_internal_error, apex_error_code, ora_sqlcode, ora_sqlerrm , error_backtrace, component_type, component_id, component_name -- substitution strings , application_id, app_page_id, app_user, browser_language , timestamp) VALUES (p_error.message, p_error.additional_info, p_error.display_location, p_error.association_type , p_error.page_item_name, p_error.region_id, p_error.column_alias, p_error.row_num , l_is_internal_error, p_error.apex_error_code, p_error.ora_sqlcode, p_error.ora_sqlerrm , p_error.error_backtrace, p_error.component.type, p_error.component.id, p_error.component.name -- substitution strings , v('APP_ID'), v('APP_PAGE_ID'), v('APP_USER'), v('BROWSER_LANGUAGE') , SYSDATE) RETURNING error_id INTO l_error_id; COMMIT; -- RETURN l_error_id; END fun_log_error;
Unexpected errors
Let’s extend the error handling function with some logic to capture unexpected errors. To do so, construct an IF statement which is made up of two conditions:
- the encountered error is recognized as an internal error
- ignore errors related to authorization verification
IF p_error.is_internal_error AND p_error.apex_error_code <> 'APEX.AUTHORIZATION.ACCESS_DENIED' THEN -- log the error and get the generated PK l_reference_id := fun_log_error(p_error => p_error); -- notify app. admin. via e-mail pro_send_email(p_reference_id => l_reference_id); -- display a generic error message l_result.message := fun_build_error_message(l_reference_id); l_result.additional_info := NULL; -- set the display location l_result.display_location := apex_error.c_inline_in_notification; ELSE ...
No rocket science here. I first log the error by calling the fun_log_error function, then I send an e-mail to the application administrator (see below), and finally I specify how the error should be shown to the end user. You might be interested in the code for pro_send_email and fun_build_error_message:
PROCEDURE pro_send_email(p_reference_id IN error_log.error_id%TYPE) AS BEGIN apex_mail.send(p_to => 'name@domain.com' , p_cc => NULL , p_from => 'name@domain.com' , p_subj => 'Unexpected error in app. ID ' || v('APP_ID') , p_body => 'Please use an e-mail client that supports HTML messages.' , p_body_html => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <style type="text/css"> body { background-color:#fff; color:#000; font-family:Arial, Helvetica, sans-serif; font-size:12px; } </style> </head> <body> <p>An unexpected error has occurred in application ID ' || v('APP_ID') || ', page ID ' || v('APP_PAGE_ID') || '.<br/> Please refer to the error log for detailed information on incident <b>#' || p_reference_id || '</b>.</p> </body> </html>'); -- apex_mail.push_queue(); END pro_send_email;
FUNCTION fun_build_gen_error_message(p_reference_id IN error_log.error_id%TYPE) RETURN VARCHAR2 AS BEGIN RETURN 'An unexpected error has occurred. ' || 'Your application administrator has been notified of the error details and ' || 'will contact you shortly about incident <b>#' || p_reference_id || '</b>.'; END fun_build_gen_error_message;
Constraint violations
Another interesting feature of the error handling function is the ability to intercept database constraint violations. All you need is a lookup table in which you couple a constraint name with an error message. You can also specify the page item name or tabular form column that may cause the constraint violation. Having this information makes it possible to append the error message with a link that focuses the page item when clicked.
In our code, p_error.ora_sqlcode contains the error code returned by the Oracle database. You can filter out constraint violations by looking after the following ORA-errors:
- ORA-00001: unique constraint violated
- ORA-02091: transaction rolled back
- ORA-02290: check constraint violated
- ORA-02291: integrity constraint violated – parent key not found
- ORA-02292: integrity constraint violated – child record found
IF p_error.ora_sqlcode IN (-1, -2091, -2290, -2291, -2292) THEN -- get the constraint name l_constraint_name := apex_error.extract_constraint_name(p_error => p_error); -- BEGIN SELECT error_message , page_item_name , tab_form_col_alias INTO l_result.message , l_result.page_item_name , l_result.column_alias FROM constraint_lookup WHERE upper(constraint_name) = upper(l_constraint_name); EXCEPTION -- not every constraint has to be in our lookup table WHEN no_data_found THEN l_reference_id := fun_log_error(p_error => p_error); -- notify app. admin. via e-mail pro_send_email(p_reference_id => l_reference_id); -- display a generic error message l_result.message := fun_build_gen_error_message(l_reference_id); END; l_result.additional_info := NULL; -- set the display location l_result.display_location := apex_error.c_inline_in_notification; END IF; ...
The built-in function apex_error.extract_constraint_name gets you the name of the violated constraint. This constraint name enables you to perform a SELECT … INTO … on the CONSTRAINT_LOOKUP table to fetch the corresponding error message data. However, it is possible that the constraint name is not present in the lookup table, hence the NO_DATA_FOUND exception handler. From that moment I treat the error as an unexpected error.
Finish it off
Complete the error handling function by including the code snippet below. It consists of two IF blocks to enhance certain error situations. More information can be found in the documentation.
- get_first_ora_error_text returns the first ORA-error message text stored in p_error.ora_sqlerrm
- auto_set_associated_item tries to automatically set the associated page item or tabular form column
IF p_error.ora_sqlcode IS NOT NULL AND l_result.message = p_error.message THEN l_result.message := apex_error.get_first_ora_error_text(p_error => p_error); END IF; -- IF l_result.page_item_name IS NULL AND l_result.column_alias IS NULL THEN apex_error.auto_set_associated_item(p_error => p_error , p_error_result => l_result); END IF; END IF; -- RETURN l_result; END fun_catch_error;
Thank you – *very* useful.
LikeLike
Question here.. In using your code .. If I have an error on a standard report, and have your function defined as the error handling function for a page, shouldn’t it handle displaying a cleaner error for a divide by zero error?
I am confused…
Thank you,
Tony Miller
Dallas, TX
LikeLike
Hey Tony,
I’m sorry for this late reply.
I guess you encountered another gap in the error handling function. There are some situations that are simply not being properly handled. It has been a while I used an error handling function, but I remember having issues with uncaught not null constraint violations. I can’t remember what went wrong though.
There is not much we can do about this I’m afraid, since all the “magic” happens behind the scenes. Let’s hope for a more reliable and solid error handling function in the future.
Nick.
LikeLike
Nick,
Thanks again for the starter code.. I am putting into my sample application that I am using to demonstrate an authentication plugin for Kscope 2013…
Do you know if they have added any improvements to APEX 4.2 in regards to exception handling??
LikeLike
I don’t think anything has changed in the area of error handling. The APEX 4.2 feature description for example does not mention anything about this topic.
LikeLike
This would be great for what I want…except that I’m not sure that ‘internal error message’ corresponds well to ‘unexpected error’ I would only want to log a failure of the application – like I log unhandled exceptions in a PL/SQL block. I don’t want to log “APEX.DATA_HAS_CHANGED” etc.
LikeLike
Question, if I want to handle a specific error type (in this case it’s APEX.AUTHORIZATION.ACCESS_DENIED) by having the application redirect to the home page, how would I do it? I’ve thought of using owa_util.redirect_url, but I’m not sure how I would get the needed parameters (App ID and Session ID). would I just use v(‘SESSION’) in the error handling function?
LikeLike
Couldn’t you handle the error locally? Or do you anticipate it happening in multiple locations? You could in the exception handling block add to the existing case statement to look for the specific error and deal with accordingly.. The App_ID and Session_ID items are available via session state variables…
Thank you,
Tony Miller
LuvMuffin Software
Ruckersville, VA
LikeLike
HELLO I’M VERY NEW IN APEX AND ORACLE, MY QUESTION IS: WHERE TO CREATE THE TABLAS AND THE FUNCTION: IN THE PARSING SCHEMA OR APEX_PUBLIC_USER AND THEN GRANT PRIVILEGES OVER THE FUNCTION TO THE PARSING SCHEMA?
LikeLike
You would build them in the parsing schema of your application… If you had it in your higher level schema, then how would you distinguish which errors to show for which application?
Thank you,
Tony Miller
LuvMuffin Software
Ruckersville, VA
LikeLike
The error handling function usually goes in the parsing schema of the APEX application. It is possible of course to create it in a a higher level schema. You would then have to grant execute privileges to the parsing schema on the error handling function.
@Tony: You can use a single error log table for multiple applications by storing the value of v(‘APP_ID’) in a column.
Nick
LikeLike
Understood, but in a lot of companies, having an application writing data in a table at the level would be a major no-no.. Better to be safe than sorry later from a auditor or admin who sees this happening..
Thank you,
Tony Miller
LuvMuffin Software
Ruckersville, VA
LikeLiked by 1 person
Hey Nick,
Thanks..This is very useful.
How i can handle the not null constraints & unique index etc..
LikeLike
Hi Chandu,
I explained that in the article under the subsection “Constraint violations”. What is your exact question or problem? By the way, I don’t think you can trap not null constraint violations using the APEX error handling function.
Nick
LikeLiked by 1 person
Thanks for your quick response Nick.
Your expectation is right here, I have a not null constraint on a table table_name.
When i try to enter something, application giving me
ORA-01400: cannot insert NULL into (“workspacename”.”tablename”.”colulmnname”)
My question is how we can handle these errors using the above function.
Thanks
LikeLike
Nick,
So you are saying that the new exception handling for APEX 4.x does NOT catch the null constraint exceptions?
Why would APEX handle just some exceptions? I would think if you add the exception information into the defined exceptions table, it would find them and provide a nicer message than the default out of the box that Oracle does..
LikeLiked by 1 person
Why I am saying this is, the application I currently help maintain DOES seem to handle the not null constraints within the exception handling and lookup table provided by the process..
LikeLike
You are right, Tony. A combination of unnamed constraints and missing Oracle error codes in the error handling function caused my tests to constantly fail. After a proper test case, I managed to trap named not null constraint violations in the error handling function.
Thanks for the help,
Nick
LikeLike
Chandu,
I had a closer look and found out that it is possible to trap not null constraint violations. Simply create a named not null constraint on the specific column and change the following line in the error handling function.
Please note that unnamed not null constraints (e.g. SYS_C0040010) are NOT being trapped, so make sure to name your not null constraints.
Nick
LikeLiked by 1 person
Is there a way to capture a particular page item value when an error occurs and write it into the error log table. For example, if a ora error occurred I would like to know which record (employee id ) was being modified when the error occurred.
LikeLike
That is possible. In the error handling function, use the v function to reference a page item’s value. For example.
Just use the v function in an insert statement to write the page item’s value to a table column.
Nick
LikeLike
Hi Nick,
Thanks for your reply..
I am facing some different scenario here..I am handling the errors in my page process using raise_application_error.
apex_error_handling function overwriting this(page level) error messages.
As per the documentation
“Note: Error handling specified at the page-level overwrites any error handling function specified here”
I dont want to overwrites my page-level error handling here. can you suggest anything for this..
LikeLike
Hello,
That sentence in the Oracle docs refers to the possibility of specifying an error handling function at page level: “If an error handling function is specified on a page then it is utilized, instead of this application level setting, for that page”.
Using raise_application_error doesn’t handle errors, it actually triggers them. I’m not sure what you are trying to achieve, but I believe you are looking for the EXCEPTION clause in your page processes to properly handle errors. More info on the EXCEPTION clause can be found here: http://www.tutorialspoint.com/plsql/plsql_exceptions.htm
The EXCEPTION clause allows you to trap ORA errors, preventing them from being processed by the error handling function. The raise_application_error procedure raises an exception and will be caught by the error handling function if it’s not getting handled in the EXCEPTION clause.
Nick
LikeLike