This is a best practice question on error handling through multiple levels of PL/SQL procedures. I've looked at a few other questions to help me out, in particular this one.
Currently, I have a program with Procedure 1, which calls Procedure 2, which calls Procedure 3. I'm trying to perform adequate error handling - but I'd like to output eventually the exact problem back to the application layer. I'm hoping to get some ideas on how I can do this efficiently and clearly.
My current solution method is below, but it seems rather messy to me, with lots of variable declarations. I am very new to PL/SQL (and SQL in general) so I'd appreciate any advice on:
Good error handling techniques when dealing with multiple layers of procedures.
Feeding error messages back up to application layer (in my procedure below, represented by "out_overall_output" variable.
Program Flow: UI -> Proc 1 -> Proc 2 -> Proc 3
Procedure 1:
--One input variable, one output.
in_id VARCHAR2;
out_overall_output VARCHAR2;
...
DECLARE
l_success BOOLEAN;
l_error_output VARCHAR2(100);
BEGIN
Proc2(id, l_success, l_error_output);
IF l_success = FALSE THEN
out_overall_output = l_error_output
END IF
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
out_overall_output:= 'Error calling Proc 2'
RETURN;
END;
--Normal flow continues if l_success is true...
Procedure 2:
-- One input variable, two output.
in_id
out_success
out_error_output
//other logic
DECLARE
l_success BOOLEAN;
l_error_output VARCHAR2(100)
BEGIN
Proc3(id, l_success, l_error_output)
IF l_success = FALSE THEN
out_error_output = l_error_output
END IF
EXCEPTION
WHEN OTHERS
out_error_output = 'Error calling Proc 3'
RETURN;
END;
Procedure 3:
--One input variable, two output.
in_id VARCHAR2;
out_success BOOLEAN;
out_error_message VARCHAR2;
...
BEGIN
DELETE
FROM table
WHERE id = in_id;
EXCEPTION
WHEN NO_DATA_FOUND THEN
out_success = FALSE;
out_error_message = 'Error - No data to delete'
WHEN OTHERS THEN
out_success = FALSE;
out_error_message = 'Error deleting data.'
END;
Note: The levels of procedure calling goes deeper than this. The snippets I have shown are greatly simplified. The error messages and variable names in my real procedures are more descriptive.
To show exact explanations of "what happens with a server" for application level you can try following. In procedures:
create or replace procedure p1 is
...
exception
when <some_error> then
<do something>
-- re-raise error:
raise_application_error(-20001, 'Client with ID '|| ID || ' has no right to perform action "' || ACTION_NAME || '"', true);
end;
create or replace procedure p2 is
begin
p1;
exception
when <another_error> then
<do something>
-- re-raise error:
raise_application_error(-20002, 'Action "' || ACTION_NAME || '" is not completed', true);
end;
create or replace procedure p3 is
begin
p2;
exception
when <another_error> then
<do something>
-- re-raise error:
raise_application_error(-20003, 'Purchasing of "' || CAR_NAME || '" cancelled', true);
end;
And in top level procedure:
create or replace procedure top_level_procedure is
begin
p1;
exception
when <one_more_error> then
<do something>
raise_application_error(-20004, dbms_utility.format_error_backtrace);
end;
After exception in p1 you will see something like this:
ORA-20003: Purchasing of "Cool red Ferrari" cancelled
ORA-20002: Action "car purchase" is not completed
ORA-20001: Client with ID 123 has no right to perform action "Spent all money of Bill Gates"
Third parameter of procedure raise_application_error with false value cuts all previous error messages. If you will use false value in procedure p3, you will see only one error message with code ORA-20003 in this example.
P. S. Also you can define your own exceptions and use them in WHEN .. THEN clause. Here you find more information and examples: https://docs.oracle.com/cd/B28359_01/appdev.111/b28370/errors.htm#LNPLS00704
P. P. S. How to log. Log procedure:
create or replace procedure log(p_log_message varchar2) is
pragma autonomous_transaction;
begin
insert into log_table(..., log_message) values (..., p_log_message);
commit;
end;
Call log procedure:
when <one_more_error> then
<do something>
log(..., dbms_utility.format_error_backtrace);
raise_application_error(-20004, dbms_utility.format_error_backtrace);
Related
I have a package with a several procedures and functions, the procedures are called from an external program and in turn they call functions.
Where is the best way to manage a Exception?
For example, Prog1 call Proc1 and Proc1 call the Funct1, if in function I have an exception ("TOO MANY ROW" or "NO DATA FOUND"), which is the best way to raise a specific customized message and return immediatally to Prog1.
In this moment I have this
PROCEDURE_1
BEGIN
*code*
CALL FUNCTION_1
*code*
EXCEPTION
WHEN NO DATA FOUND THEN
*print customized message*
RETURN;
END;
FUNCTION_1
BEGIN
*code*
EXCEPTION
WHEN NO DATA FOUND THEN
RAISE;
END;
It is the best way?
Regards,
Marco
Ideally a procedure would be allowed to fail with an exception and error stack if something went totally wrong, but then the difference between a fatal error and an expected condition will vary depending on the business logic you are implementing, so it's hard to say what should happen in your particular case.
If the agreed interface is that the procedure should return a formatted message, and that message may include expected business conditions such as a product being out of stock, then you can handle that within the procedure using something like this (nonsense pseudocode to illustrate approach only):
create or replace procedure procedure_1
( p_result_message out varchar2 )
as
somevar number;
begin
do_stuff('fruit','cake');
begin
somevar := function_1('bananas');
exception
when no_data_found then
p_result_message := 'No kittens are available for this mission.';
return;
end;
p_result_message := 'Cake is available in aisle 3';
exception
when something_else then
p_result_message := 'Self-destruct sequence initiated.';
end;
(For a more purist approach you might prefer to rearrange the code so that it always reaches the end with a value for p_result_message, rather than quitting partway through if some condition pops up.)
Now you have a way to handle whatever exceptions might reasonably arise in function_1, without any special handling within the function itself.
You could also have the function raise an exception defined in a package, although then you lose the ability to define a diagnostic error message at the point of failure, and in my experience this just tends to complicate things. But to illustrate:
create or replace package starfleet
as
shield_failure exception;
warp_core_malfunction exception;
pragma exception_init(shield_failure, -20998);
pragma exception_init(warp_core_malfunction, -20999);
procedure check_status
( status_message out varchar2 );
function status
return number;
end starfleet;
create or replace package body starfleet as
function status
return number
is
status_ind number;
begin
select 1 into status_ind from dual where 1=2; -- fails with NDF
return status_ind;
exception
when no_data_found then raise shield_failure;
end status;
procedure check_status
( status_message out varchar2 )
is
status_ind number;
begin
status_ind := status();
status_message := 'Everything is fine';
exception
when warp_core_malfunction then status_message := 'Abandon ship';
when shield_failure then status_message := 'Increase power to shields';
end check_status;
end starfleet;
Now, the status() function can raise exceptions defined in the package (or any other package for that matter), and procedure check_status can catch it and decide how to handle it.
Example of calling it from SQL*Plus:
SQL> var status_message varchar2(100)
SQL>
SQL> begin
2 starfleet.check_status(:status_message);
3 end;
4 /
PL/SQL procedure successfully completed.
STATUS_MESSAGE
-------------------------
Increase power to shields
I have procedure like below, but when block is run it does not shows message for error if data is not found.
CREATE OR REPLACE
PROCEDURE DDPROJ_SP
(P_IDPROJ IN DD_PROJECT.IDPROJ%TYPE,
P_INFO OUT VARCHAR2,
p_check OUT VARCHAR2)
IS
CURSOR cur_ddproj IS
SELECT *
FROM dd_project
WHERE idproj = p_idproj;
lv_projinfo_txt VARCHAR2(100);
BEGIN
FOR rec_ddproj IN cur_ddproj LOOP
lv_projinfo_txt := (rec_ddproj.idproj||', '||rec_ddproj.projname||
', '||rec_ddproj.projstartdate||', '||rec_ddproj. projenddate||
', '||rec_ddproj.projfundgoal||', '||rec_ddproj.p rojcoord);
END LOOP;
P_INFO := LV_PROJINFO_TXT;
EXCEPTION
WHEN NO_DATA_FOUND THEN
P_CHECK :='Please select another project';
DBMS_OUTPUT.PUT_LINE(P_CHECK);
END;
And block:
DECLARE
LV_INFO_TXT VARCHAR2(100);
LV_CHECK_TXT VARCHAR2(30);
BEGIN
DDPROJ_SP(00,lv_info_txt,lv_check_txt);
DBMS_OUTPUT.PUT_LINE(LV_INFO_TXT);
END;
After RUNNING BLOCK IF id provided is correct I would receive requested information but if ID is not found exception will not show message on print.
Firstly, as has been pointed out, your exception handler doesn't do anything really visible except call dbms_output, the results of which you'll only see if you set serverout on or otherwise access the results from dbms_output.
Secondly and more importantly, when you use a FOR loop to process the results of a cursor, the NO_DATA_FOUND exception will never be raised.
If you want to detect no rows found, you have a few options:
After the loop, check if the variable was set, e.g.:
...
end loop;
if lv_projinfo_txt is null then
raise no_data_found;
end if;
If you don't expect more than 1 record to be found by the query (which is suggested by your predicate on an "id"), you can avoid the FOR loop and use a simple select into:
PROCEDURE DDPROJ_SP
(P_IDPROJ IN DD_PROJECT.IDPROJ%TYPE,
P_INFO OUT VARCHAR2,
p_check OUT VARCHAR2)
IS
rec_ddproj dd_project%rowtype;
lv_projinfo_txt VARCHAR2(100);
BEGIN
SELECT *
into rec_ddproj
FROM dd_project
WHERE idproj = p_idproj;
lv_projinfo_txt := (rec_ddproj.idproj||', '||rec_ddproj.projname||
', '||rec_ddproj.projstartdate||', '||rec_ddproj.projenddate||
', '||rec_ddproj.projfundgoal||', '||rec_ddproj.projcoord);
P_INFO := LV_PROJINFO_TXT;
EXCEPTION
WHEN NO_DATA_FOUND THEN
P_CHECK :='Please select another project';
DBMS_OUTPUT.PUT_LINE(P_CHECK);
END;
Notes:
A select into may raise NO_DATA_FOUND or TOO_MANY_ROWS.
Good practice is to never handle errors without re-raising the exception, unless your code actually handles the exception. In your case, your code merely sends a signal back to the calling process via the p_check parameter, which moves responsibility for handling the error to the caller. This might be ok in some circumstances but it assumes the caller actually heeds the signal. It would be better to raise an exception which forces the caller to handle it appropriately.
Good practice is to alias all columns and parameters in a query; having a SQL predicate like idproj = p_idproj makes the assumption that the table will never have a column called p_idproj in the future. Instead, it's good practice to deliberately alias all columns and parameters, e.g.
SELECT x.*
into rec_ddproj
FROM dd_project x
WHERE x.idproj = ddproj_sp.p_idproj;
We are using FORALL.....SAVE EXCEPTIONS. At the end of the loop, we have this:
FOR i IN 1..SQL%BULK_EXCEPTIONS.COUNT LOOP<BR><BR>
DBMS_OUTPUT.PUT_LINE('ERROR CREATING STAGING TICKER: ' || SQLERRM(-SQL%BULK_EXCEPTIONS(i).ERROR_CODE));<BR><BR>
DBMS_OUTPUT.PUT_LINE('INDEX INFO: ' || SQL%BULK_EXCEPTIONS(i).ERROR_INDEX);<BR>
END LOOP;
Is there any way for me to get at actual VALUES in that array? Say if a customers email was too long.....for me to actually display the value which caused the error? Rather than just some index number?
Thanks!
You can use the loop variable i to display the content of the exception array in your case. See below an example procedure:
CREATE OR REPLACE PROCEDURE PROC1 (V_EMP_ID DBMS_SQL.NUMBER_TABLE)
IS
lv_error_string VARCHAR2(4000);
BEGIN
FORALL INDX IN V_EMP_ID.FIRST..V_EMP_ID.LAST SAVE EXCEPTIONS
UPDATE EMPLOYEES
---trying to rasie an exception by using a calculation
SET SALARY=SALARY * 99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
WHERE ID_E= V_EMP_ID(INDX);
EXCEPTION
WHEN OTHERS
THEN
FOR i IN 1 .. SQL%BULK_EXCEPTIONS.COUNT
LOOP
---Am printing the value of the exception array.
dbms_output.put_line('exception Raised for record' ||V_EMP_ID(i));
END LOOP;
END;
/
Ouput:
SQL> DECLARE
empid DBMS_SQL.NUMBER_TABLE;
BEGIN
empid (1) := 1;
empid (2) := 9;
PROC1 (empid);
END;
/
exception Raised for record 1
PL/SQL procedure successfully completed.
I wrote a procedure in PL/SQL to delete rows from a table,However,if that record does not exist,then throws some error like this: DBMS_OUTPUT.PUT_LINE('No such record'); My procedure is:
CREATE OR REPLACE PROCEDURE del_cn2
(c_cntry_id IN COUNTRIES.COUNTRY_ID%TYPE
)
IS
v_error_code NUMBER;
BEGIN
DELETE from countries
WHERE country_id =c_cntry_id;
IF SQL%NOTFOUND THEN
DBMS_OUTPUT.PUT_LINE('No such record');
END IF;
EXCEPTION WHEN OTHERS THEN
v_error_code :=SQLCODE;
IF v_error_code =-2292 THEN
RAISE_APPLICATION_ERROR(-20004,'Organization '||TO_CHAR(c_cntry_id)||' site
details defined for it.');
END IF;
END;
/
However,when I execute this procedure and provide a record that does not exist in my table,it gives message "Procedure completed successfully" I am using this to execute:
Execute procedure del_cn2('JJ');
Can someone please suggest?
If you want an exception to be thrown when a value that does not exist in the table is passed in, you would need to actually throw an exception. You shouldn't use dbms_output for any sort of error output. That is a very simplistic debugging tool-- you shouldn't assume that the caller will ever be able to see that output.
My guess is that you want something like
CREATE OR REPLACE PROCEDURE del_cn2
(c_cntry_id IN COUNTRIES.COUNTRY_ID%TYPE
)
IS
BEGIN
DELETE from countries
WHERE country_id =c_cntry_id;
IF SQL%ROWCOUNT = 0
THEN
raise_application_error( -20001, c_cntry_id || ' no such value.' );
END IF;
END;
try to set serverout to ON
example:
create table tst_delete (col1 int);
create procedure p_test_delete as
BEGIN
DELETE FROM tst_delete
WHERE col1 = 1;
IF (SQL%NOTFOUND)
THEN
dbms_output.put_line('No records found');
END IF;
END;
then call the procedure in SqlPlus
SQL> exec p_test_delete;
PL/SQL procedure successfully completed
same issue that you described - no insformation...
next try with output activated
SQL> set serverout on
SQL> exec p_test_delete;
No records found
PL/SQL procedure successfully completed
SQL>
Currently I'm using the following pattern to record error details in PL/SQL exception propagation. Please see the commented code below for the details. I'm happy with it as the error handling code doesn't clutter the whole code base and all the details why an error was triggered can be recorded.
Unfortunately there is an unwanted side-effect of package state introduced by v_error variable.
How I can pass an error detail information in PL/SQL exception propagation without introducing a package state ? (I want to eliminate package state to make deployment easier.)
Using different exceptions like rule_2_failure_ex and rule_3_failure_ex is not the solution I'm looking for as
there is no need to handle the error conditions differently
for troubleshooting it is very important to be able to record arbitrary information
(I'm already using a logging so the error condition information is available, but it's not in "the right place".)
I'm looking for Oracle 11g solution but 12c solution (if different than in 11g) is also welcome as one day I might end working with 12c too (personally I don't care about 10g).
-- this is a simplied example to address to question only
create or replace package so50 is
procedure run(p_num in number);
end;
/
show errors
create or replace package body so50 is
processing_failure_ex exception;
-- package state that I'd like to eliminate
v_error varchar2(32767);
-- in reality the processing and details are more complex
procedure p3(p_num in number) is
begin
if p_num = 3
then
-- it's important to be able to record arbitrary information at this point
v_error := 'Failed to process rule 3: (p_num = ' || p_num || ')';
raise processing_failure_ex;
end if;
end;
-- the comments on p3 apply
procedure p2(p_num in number) is
begin
if p_num = 2
then
v_error := 'Failed to process rule 2: (p_num = ' || p_num || ')';
raise processing_failure_ex;
end if;
end;
procedure p1(p_num in number) is
begin
p2(p_num);
p3(p_num);
exception
when others then
v_error := v_error
|| ' Additional details of failure.';
raise;
end;
procedure run(p_num in number) is
begin
v_error := null;
begin
p1(p_num);
exception
when processing_failure_ex then
-- in reality an error recovery will be tried first and only then
-- the error will be forwarded to a monitoring framework that will
-- raise an alert for human action
dbms_output.put_line('Error details: ' || v_error);
raise;
end;
exception
when others then
-- out of the scope of the question
raise;
end;
end;
/
show errors
You could use raise_application_error with an error code tied to your exception:
create or replace package body so50 is
processing_failure_ex exception;
pragma exception_init(processing_failure_ex, -20999);
And raise with the message you want:
raise_application_error(-20999,
'Failed to process rule 3: (p_num = ' || p_num || ')', true);
When you want to store the whole stack you can use dbms_utility.format_error_stack:
dbms_output.put_line('Error details:');
dbms_output.put_line(dbms_utility.format_error_stack);
So removing v_error altogether:
create or replace package so50 is
procedure run(p_num in number);
end;
/
create or replace package body so50 is
processing_failure_ex exception;
pragma exception_init(processing_failure_ex, -20999);
-- in reality the processing and details are more complex
procedure p3(p_num in number) is
begin
if p_num = 3
then
-- it's important to be able to record arbitrary information at this point
raise_application_error(-20999,
'Failed to process rule 3: (p_num = ' || p_num || ')', true);
end if;
end;
-- the comments on p3 apply
procedure p2(p_num in number) is
begin
if p_num = 2
then
raise_application_error(-20999,
'Failed to process rule 2: (p_num = ' || p_num || ')', true);
end if;
end;
procedure p1(p_num in number) is
begin
p2(p_num);
p3(p_num);
exception
when others then
raise_application_error(-20999,
'Additional details of failure', true);
end;
procedure run(p_num in number) is
begin
begin
p1(p_num);
exception
when processing_failure_ex then
-- in reality an error recovery will be tried first and only then
-- the error will be forwarded to a monitoring framework that will
-- raise an alert for human action
dbms_output.put_line('Error details:');
dbms_output.put_line(dbms_utility.format_error_stack);
raise;
end;
exception
when others then
-- out of the scope of the question
raise;
end;
end;
/
Calling that gets:
SQL> set serveroutput on
SQL> exec so50.run(1);
PL/SQL procedure successfully completed.
SQL> exec so50.run(2);
ORA-20999: Additional details of failure
ORA-06512: at "STACKOVERFLOW.SO50", line 42
ORA-20999: Failed to process rule 2: (p_num = 2)
ORA-06512: at "STACKOVERFLOW.SO50", line 64
ORA-06512: at line 1
Error details:
ORA-20999: Additional details of failure
ORA-06512: at "STACKOVERFLOW.SO50", line 42
ORA-20999: Failed to process rule 2: (p_num = 2)
SQL> exec so50.run(3);
ORA-20999: Additional details of failure
ORA-06512: at "STACKOVERFLOW.SO50", line 42
ORA-20999: Failed to process rule 3: (p_num = 3)
ORA-06512: at "STACKOVERFLOW.SO50", line 64
ORA-06512: at line 1
Error details:
ORA-20999: Additional details of failure
ORA-06512: at "STACKOVERFLOW.SO50", line 42
ORA-20999: Failed to process rule 3: (p_num = 3)
In both cases the stack trace before the 'Error details:' is coming from the final out-of-scope raise; if that was temporarily squashed (just for a demo, not suggesting you really squash it!) you'd just see:
SQL> exec so50.run(3);
PL/SQL procedure successfully completed.
Error details:
ORA-20999: Additional details of failure
ORA-06512: at "STACKOVERFLOW.SO50", line 42
ORA-20999: Failed to process rule 3: (p_num = 3)
You can use different exception numbers for the various procedures and scenarios of course, I've just used a common one to simplify things for now. They only need to be named (tied with a name via a pragma) if you want to catch them by name. And if you do you could have all the exceptions defined in one place.
My current solution based on #AlexPoole answer:
Exception Package
-- encapsulates the uglyness to keep calling code clean
create or replace package so50_ex is
-- exception type and error code reserved for this purpose only
general_ex exception;
general_ex_code constant number := -20999;
pragma exception_init(general_ex, -20999);
procedure raise(p_msg in varchar2, p_ex_code in number default -20999);
function full_error_stack return varchar2;
end;
/
show errors
create or replace package body so50_ex is
procedure raise(p_msg in varchar2, p_ex_code in number default -20999) is
begin
raise_application_error(p_ex_code,
substrb(p_msg, 1, 2048),
true);
end;
function full_error_stack return varchar2 as
-- will always fit to buffer as format_error_stack returns 2000 bytes at
-- maximum
v_stack varchar2(32767) := dbms_utility.format_error_stack;
begin
-- might not fit to buffer as format_error_backtrace return length is not
-- limited
v_stack := v_stack ||
substrb(dbms_utility.format_error_backtrace, 1, 30767);
return v_stack;
end;
end;
/
show errors
Usage Example
create or replace package so50 is
-- a user can always have his own exceptions
processing_failure_ex exception;
processing_failure_ex_code constant number := -20100;
pragma exception_init(processing_failure_ex, -20100);
procedure run(p_num in number);
end;
/
show errors
create or replace package body so50 is
procedure p3(p_num in number) is
begin
if p_num = 3
then
-- use specific exception
so50_ex.raise('Failed to process rule 3: (p_num = ' || p_num || ')',
processing_failure_ex_code);
end if;
end;
procedure p2(p_num in number) is
begin
if p_num = 2
then
-- use default exception
so50_ex.raise('Failed to process rule 2: (p_num = ' || p_num || ')');
end if;
end;
procedure p1(p_num in number) is
begin
p2(p_num);
p3(p_num);
exception
when processing_failure_ex then
dbms_output.put_line('ERROR RECOVERED SUCCESFULLY.');
dbms_output.put_line('DETAILS:');
dbms_output.put_line(so50_ex.full_error_stack);
when others then
so50_ex.raise('Additional details of failure.');
end;
procedure run(p_num in number) is
begin
p1(p_num);
exception
when others then
dbms_output.put_line('EXCEPTION: ' || so50_ex.full_error_stack);
raise;
end;
end;
/
show errors