Skip to main content

Custom APEX error handling function

W

When your app is ready for users, you want to mask (trap/catch, translate, log) error messages which slipped throught your standard PL/SQL error handler and show user something less revealing. You might also want to store these errors in your own error log table and not in the APEX log.

To achieve this you need to define an Error Handling Function in Shared Components, Application Definition Attributes, Error Handling. Use your own name, mine is "apex_error_handling".


To create this function you can get inspired like me by the nice example in Oracle documentation, in APEX_ERROR package, section Example of an Error Handling Function. I modified this function a bit to serve my needs:

  • show whole error message with backtrace to APEX developers
  • show just a error type and log_id to common users
  • show constraint names on integrity errors (you can easily convert them to relevant table and columns)
  • log everything (specially the component causing the error and the error backtrace) to my table
  • let the application exception throught (for me SQLCODE=-20000)


Main goal of this function is to transform record APEX_ERROR.T_ERROR (p_error) on the input to record APEX_ERROR.T_ERROR_RESULT. You can find records description in section Constants and Attributes except the t_error.component record. Structures follows:

type t_error is record (
    message                  varchar2(32767),   -- Error message which will be displayed
    additional_info          varchar2(32767),   -- Only used for display_location ON_ERROR_PAGE to display additional error information
    display_location         varchar2(40),      -- Use constants "used for display_location" below
    association_type         varchar2(40),      -- Use constants "used for asociation_type" below
    page_item_name           varchar2(255),     -- Associated page item name
    region_id                number,            -- Associated tabular form region id of the primary application
    column_alias             varchar2(255),     -- Associated tabular form column alias
    row_num                  pls_integer,       -- Associated tabular form row
    apex_error_code          varchar2(255),     -- Contains the system message code if it's an error raised by APEX
    is_internal_error        boolean,           -- Set to TRUE if it's a critical error raised by the APEX engine, like an invalid SQL/PLSQL statements, ... Internal Errors are always displayed on the Error Page
    is_common_runtime_error  boolean,           -- TRUE for internal authorization, session and session state errors that normally should not be masked by an error handler
    ora_sqlcode              number,            -- SQLCODE on exception stack which triggered the error, NULL if the error was not raised by an ORA error
    ora_sqlerrm              varchar2(32767),   -- SQLERRM which triggered the error, NULL if the error was not raised by an ORA error
    error_backtrace          varchar2(32767),   -- Output of sys.dbms_utility.format_error_backtrace or sys.dbms_utility.format_call_stack
    error_statement          varchar2(32767),   -- Statement that was parsed when the error occurred - only suitable when parsing caused the error
    component                apex.t_component   -- Component which has been processed when the error occurred
type t_error_result is record (
    message           varchar2(32767),  -- Error message which will be displayed
    additional_info   varchar2(32767),  -- Only used for display_location ON_ERROR_PAGE to display additional error information
    display_location  varchar2(40),     -- Use constants "used for display_location" below
    page_item_name    varchar2(255),    -- Associated page item name
    column_alias      varchar2(255)     -- Associated tabular form column alias

The hidden definition:

type t_component is record (
    type varchar2(30),      -- APEX dictionary view name of the component where an error occurred. For example APEX_APPLICATION_PAGE_PROC
    id   number,            -- Internal component id which triggered the error. The id is always the id of the primary application
    name varchar2(32767)    -- Name of the component which triggered the error like the process name


The function

Now you are ready to create your function:

CREATE OR REPLACE FUNCTION apex_error_handling (
    p_error             APEX_ERROR.T_ERROR
)
RETURN APEX_ERROR.T_ERROR_RESULT
AS
    out_result          APEX_ERROR.T_ERROR_RESULT;
    --
    l_log_id            NUMBER;                 -- log_id from your log_error function (returning most likely sequence)
    l_log_name          VARCHAR2(64);           -- short error type visible to user
    l_component         VARCHAR2(64);           -- to identify source component in your app
    --
    app_exception       CONSTANT NUMBER         := -20000;  -- your app exception code
BEGIN
    out_result := APEX_ERROR.INIT_ERROR_RESULT(p_error => p_error);

    -- assign log_id sequence (app specific, probably from sequence)
    IF p_error.ora_sqlcode IN (-1, -2091, -2290, -2291, -2292) THEN
        -- handle constraint violations
        -- ORA-00001: unique constraint violated
        -- ORA-02091: transaction rolled back (can hide a deferred constraint)
        -- ORA-02290: check constraint violated
        -- ORA-02291: integrity constraint violated - parent key not found
        -- ORA-02292: integrity constraint violated - child record found
        l_log_name := 'CONSTRAINT_ERROR' || '|' || APEX_ERROR.EXTRACT_CONSTRAINT_NAME (
            p_error             => p_error,
            p_include_schema    => FALSE
        );
        --
        out_result.message          := l_log_name;
        out_result.display_location := APEX_ERROR.C_INLINE_IN_NOTIFICATION;
        --
    ELSIF p_error.is_internal_error THEN
        l_log_name := 'INTERNAL_ERROR';
    ELSE
        l_log_name := 'UNKNOWN_ERROR';
    END IF;

    -- store incident in your log
    l_component := TO_CHAR(APEX_APPLICATION.G_FLOW_STEP_ID) || '|' || REPLACE(p_error.component.type, 'APEX_APPLICATION_', '') || '|' || p_error.component.name;
    --
    l_log_id := log_error (
        l_log_name,
        l_component,
        p_error.ora_sqlerrm,
        p_error.error_statement,
        p_error.error_backtrace
    );

    -- mark associated page item (when possible)
    IF out_result.page_item_name IS NULL AND out_result.column_alias IS NULL THEN
        APEX_ERROR.AUTO_SET_ASSOCIATED_ITEM (
            p_error         => p_error,
            p_error_result  => out_result
        );
    END IF;

    -- show only the latest error message to common users
    IF (is_developer() OR p_error.ora_sqlcode = app_exception) THEN
        out_result.message := l_log_name || '|' || TO_CHAR(l_log_id) || '<br />' ||
            l_component || '<br />' ||
            out_result.message || '<br />' ||
            APEX_ERROR.GET_FIRST_ORA_ERROR_TEXT(p_error => p_error);
        out_result.additional_info := '';
    ELSE
        out_result.message          := l_log_name || '|' || TO_CHAR(l_log_id);
        out_result.additional_info  := '';
    END IF;
    --
    RETURN out_result;
END;
/


Here is a helping function to recognize if the current user is developer or not. You can remove it if you don't need it.

CREATE OR REPLACE FUNCTION is_developer (
    in_username         VARCHAR2        := NULL
)
RETURN BOOLEAN AS
    is_valid            VARCHAR2(1);
BEGIN
    SELECT 'Y' INTO is_valid
    FROM apex_workspace_developers d
    JOIN apex_applications a
        ON a.workspace                  = d.workspace_name
    WHERE a.application_id              = sess.get_app_id()
        AND d.is_application_developer  = 'Yes'
        AND d.account_locked            = 'No'
        AND COALESCE(in_username, sess.get_user_id()) IN (UPPER(d.user_name), LOWER(d.email))
    GROUP BY d.email;
    --
    RETURN (is_valid = 'Y');
END;
/


Here is a mockup of the custom log function.

CREATE OR REPLACE FUNCTION log_error (
    in_log_name         logs.log_name%TYPE,
    in_component        logs.component%TYPE,
    in_message          logs.message%TYPE       := NULL,
    in_statement        logs.statement%TYPE     := NULL,
    in_backtrace        logs.backtrace%TYPE     := NULL
)
RETURN logs.log_id%TYPE
AS
    PRAGMA AUTONOMOUS_TRANSACTION;
    --
    rec                 logs%ROWTYPE;
BEGIN
    rec.log_id          := log_id.NEXTVAL;      -- your sequence
    rec.log_name        := in_log_name;
    rec.component       := in_component;
    rec.message         := in_message;
    rec.statement       := in_statement;
    rec.backtrace       := in_backtrace;
    --
    INSERT INTO log_errors VALUES rec;
    COMMIT;
    --
    RETURN rec.log_id;
EXCEPTION
WHEN OTHERS THEN
    ROLLBACK;
    RAISE_APPLICATION_ERROR(-20000, 'LOG_FAILED', TRUE);
END;
/


Modify it however you like and dont't forget to put these functions into a package.


Comments

  1. Thank you.it is very clear.
    why do you need the function called log_error ?what info does it adding to the developer ?
    all the info already exists in the table APEX_DEBUG_MESSAGES

    ReplyDelete
    Replies
    1. It is my personal preference. You might log using APEX feature, you might log throught Logger or into custom table. If you wrap this in the log_error function, you can switch the target anytime without changing the whole application.

      Delete

Post a Comment