I am fairly new to PL/SQL and am trying to implement some exception handling for a package I have written.
I have come across a situation where I have multiple exceptions that may be raised.
In the event these exceptions are raised I may want to do something specific for each of the exceptions, for example if exception A is raised then close and delete a file, but if exception B is raised then I need only close a cursor and send an email to warn someone that a fatal error has occurred.
This is fine and I understand how to use WHEN <EXCEPTION_NAME> THEN.
The problem I am having is that I can't write generic code which occurs for any exception raised to do something I will always want done in the event of an exception, for example writing to the log file. This means that I have to duplicate lines of code for each exception type as shown below.
DECLARE
test_exception EXCEPTION;
BEGIN
--some code
RAISE test_exception;
--some code
EXCEPTION
WHEN test_exception THEN
SEND_EMAIL('Something went wrong');
WRITE_TO_LOG('Error ' || SQLCODE || ' | ' || SUBSTR(SQLERRM, 1, 200) || ' | ' || DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);
WHEN OTHERS THEN
SOME_OTHER_FUNCTION();
WRITE_TO_LOG('Error ' || SQLCODE || ' | ' || SUBSTR(SQLERRM, 1, 200) || ' | ' || DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);
END;
What I would like to achieve is something similar to this, which does not compile.
DECLARE
test_exception EXCEPTION;
BEGIN
--some code
RAISE test_exception;
--some code
EXCEPTION
WRITE_TO_LOG('Error ' || SQLCODE || ' | ' || SUBSTR(SQLERRM, 1, 200) || ' | ' || DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);
WHEN test_exception THEN
SEND_EMAIL('Something went wrong');
WHEN OTHERS THEN
SOME_OTHER_FUNCTION();
END;
Obviously this example isn't complete but gives a rough idea. For a single duplicated line like this it isn't a problem, but if there are many cursors or files to close or other housekeeping this seems a bit verbose/tedious.
Is there a WHEN ALL THEN clause or similar? Would this be a use case for some kind of GOTO? Or am I trying to apply the wrong sort of concept here.
I can't seem to find people asking the same question which to me means I am approaching the issue in the wrong way or I am missing some basic knowledge.
Thanks
PL/SQL will only execute one exception block. So if you have some generic code (e.g. logging) you want to run for every exception, you have to it do in when others.
Then check the sqlcode to do exception specific handling. For example:
begin
raise TOO_MANY_ROWS;
exception
when others then
dbms_output.put_line ( 'Generic stuff' );
case sqlcode
when -1422 then
dbms_output.put_line ( 'TMR specific' );
when -1476 then
dbms_output.put_line ( 'ZD specific' );
else
null;
end case;
raise;
end;
/
Generic stuff
TMR specific
ORA-01422: exact fetch returns more than requested number of rows
begin
raise ZERO_DIVIDE;
exception
when others then
dbms_output.put_line ( 'Generic stuff' );
case sqlcode
when -1422 then
dbms_output.put_line ( 'TMR specific' );
when -1476 then
dbms_output.put_line ( 'ZD specific' );
else
null;
end case;
raise;
end;
/
Generic stuff
ZD specific
ORA-01476: divisor is equal to zero
Now, whether this is a good idea is debatable.
when others exception blocks should re-raise the exception in some way. This to prevent you from suppressing serious unexpected errors (like out of disk space). But for some specific exceptions you may want to carry on processing.
Addendum
If you have user-defined exceptions, you need to do a bit of extra work if you wand to handle these. One way to do it is to create a standard exceptions package. In it, name, initialize, and define value constants for all the exceptions you'll use.
You can then use these definitions for user-defined exceptions:
create or replace package excepts as
user_defined exception ;
user_defined_val pls_integer := -20001;
pragma exception_init ( user_defined, -20001 );
end;
/
begin
raise excepts.USER_DEFINED;
exception
when others then
dbms_output.put_line ( 'Generic stuff' );
case sqlcode
when -1422 then
dbms_output.put_line ( 'TMR specific' );
when -1476 then
dbms_output.put_line ( 'ZD specific' );
when excepts.user_defined_val then
dbms_output.put_line ( 'User-defined specific' );
else
null;
end case;
raise;
end;
/
Generic stuff
User-defined specific
ORA-20001:
If I'am undestanding correctly - solution is very simple. When you raising an exception, you can handle it in nested block and pass to the outer block by RAISE
DECLARE
test_exception EXCEPTION;
begin
RAISE test_exception;
--some code
EXCEPTION
WHEN OTHERS THEN
BEGIN
dbms_output.put_line( 'WRITE_TO_LOG');
RAISE;
EXCEPTION
WHEN test_exception THEN
dbms_output.put_line( 'test_exception');
WHEN OTHERS THEN
dbms_output.put_line( 'some other function');
end;
END;
output:
WRITE_TO_LOG
send mail
Related
A procedure calls a function. It works with several tables, so I devided it to blocks. All blocks has its own exception section where the specific error message can be sent via e-mail.
Now I want to make the code cleaner and pass the exception in this order:
Block -> Its Function -> Caller Procedure
I can do it with 'raise' but in this case the caller procedure doesn't know where the exception came from.
Another solution I think is that the first block would use raise_application_error(User-Error-Id, 'Specific error message'). But in this case the original SQLCODE is hidden by raise_application_error.
How can I solve this?
I have something like this:
procedure main
...
begin
v_pcs := calc_pcs(parameters);
exception
When others then
dbms_output.put_line(SCLCODE || ' - ' || SQLERRM);
-- Here I'd like to read the part of the code where the exception happened
end;
funcion calc_pcs(
parameters
) as
begin
-- first block
begin
v_return := 5 / 0; -- just an example of an error
exception
when others then
raise -- How to add text to predefined exception. For example 'FirstBlock || SqlErrM'
end;
...
rest of the code contains additional blocks with their exception handling and
there is other code without blocks
...
return v_return;
Exception
when others then
raise;
end;
Thanks Sayan! Sorry, but I may have missed something important in my previous post, let me clarify.
I want to concatenate a placeholder text to the original predefined exception string of the start block, for example "First Block" || "divisor is equal to zero". Further exception handling would pass this along with the SQLCODE unchanged.
You can do like this:
declare
e1 exception;
e2 exception;
e3 exception;
pragma exception_init(e1, -20201);
pragma exception_init(e2, -20202);
pragma exception_init(e3, -20203);
procedure func ( x int, txt varchar2 ) is
begin
case x
when 1 then raise_application_error(-20201, 'x = ['||x||'] error text 1: ' || txt);
when 2 then raise_application_error(-20202, 'x = ['||x||'] error text 2: ' || txt);
when 3 then raise_application_error(-20203, 'x = ['||x||'] error text 3: ' || txt);
else null;
end case;
end func;
begin -- main block:
func(2, 'test');
exception
when e1 then
dbms_output.put_line('e1');
dbms_output.put_line(SQLCODE || ' - ' || SQLERRM);
when e2 then
dbms_output.put_line('e2');
dbms_output.put_line(SQLCODE || ' - ' || SQLERRM);
when e3 then
dbms_output.put_line('e3');
dbms_output.put_line(SQLCODE || ' - ' || SQLERRM);
end;
/
DBFiddle: https://dbfiddle.uk/s7HeZpNB
dbms_output:
e2
-20202 - ORA-20202: x = [2] error text 2: test
Please let me know whether it is possible to catch multiple exception at same time in oracle. Not like 1 user defined and 1 is oracle default .I need to catch multiple user defined exception at same time . Kindly let me know how to do .
Thank you !
Certainly, there is - if I understood the question correctly. It is called WHEN OTHERS. Though, people usually misuse it, especially when they use
exception
when others then
null;
end;
as it successfully hides any errors that might appear. WHEN OTHERS is OK during development process, but might be really bad in production, especially if it doesn't contain raise.
Yes, you can do what you want from "When Others" as indicated by #Littlefoot or bulk processing errors (not covered here). But additionally you can have an OR condition exception name clause on the WHEN . It's not very useful as generally requires more code the 2 separate WHEN condition, but it is valid syntax. The following demonstrates various error definition methods and exception processing.
create table except_demo ( id integer, col1 varchar2(20));
insert into except_demo (id, col1)
select 5,'OK' from dual union all
select 6,'Too Many' from dual union all
select 6,'Still' from dual;
select id, count(*) from except_demo group by id;
create or replace procedure catcher(which_in integer, select_in boolean default False)
is
e_user_1 exception;
e_user_2 exception;
invalid_which_range exception;
appErr_inactive_acct exception;
sample_ora_error exception;
pragma exception_init (sample_ora_error, -00060);
rae exception;
rae_num constant integer := -20100;
pragma exception_init (rae, -20100);
col1_value except_demo.col1%type;
begin
dbms_output.put( 'Enter catcher(' || which_in || ') Result=>');
if which_in > 8
then raise invalid_which_range;
end if ;
if select_in
then
select col1
into col1_value
from except_demo
where id = which_in;
dbms_output.put_line('Select Successful 1 row selected.');
else
case which_in
when 1 then raise e_user_1;
when 2 then raise e_user_2;
when 3 then raise appErr_inactive_acct;
when 4 then raise sample_ora_error;
else raise_application_error(rae_num, 'Which_In=' || which_in || ' invalid. Please specify number 1-7 only');
end case;
end if;
exception
when e_user_1
then dbms_output.put_line('Error e_user_1'); -- user error
when e_user_2
then dbms_output.put_line('Error e_user_2');
when no_data_found or too_many_rows
then dbms_output.put_line('Select except_demo where id=' || which_in ||'; Returned 0 or more than 1 row. Must return exactly 1.' ); -- oracle predefined error
when sample_ora_error
then dbms_output.put_line('Ora Error:: ' || sqlerrm ); -- oracle error NOT predefined
when appErr_inactive_acct
then dbms_output.put_line('Error Account id ' || which_in || ' is inactive.'); -- user error
when rae
then dbms_output.put_line(sqlerrm);
end catcher;
declare
do_select boolean;
begin
for i in 1..9
loop
do_select := (i between 5 and 7);
catcher(i,do_select);
end loop;
exception
when others
then
dbms_output.put_line('Error returned from catcher=>' || sqlerrm);
raise;
end ;
drop procedure catcher;
drop table except_demo;
In a live environment the dbms_output statement would be replaced writing the message and other information to a exception log table and NOT dbms_output.
I have a very minor disagreement with Littlefoot. I firmly believe that what ever is written in development, whether intended or not, will run in production. Too often it is the unintended that gets you into trouble. Therefore the example of a misused WHEN OTHERS is invalid even in development.
Is there a variable like SQLERRM or SQLCODE that holds the statement which raised the error?
example:
/*
if some error raised from this code
and I want to know which statement cause the failure..
I wish to use some oracle varaible to know it
*/
begin
select * from t1;
select * from t2;
exception when others
dbms_output.put_line(sqlerrm || ' raised from this statement:' || <some_variable>;
end;
-- excepted result: no data found raised from this statement: select * from t2
Simple answer, no. You're losing some information by defining an exception handler. With an unhandled exception you'd get an error message which includes the line number. But obviously we need to handle errors, log them, etc. So not having a line number is pretty rubbish.
Fortunately there are a couple of options. In older versions of Oracle we can use dbms_utility.format_error_backtrace() and dbms_utility.format_error_stack() to get some useful information, including the line numbers. It's pretty unwieldy and (especially for the backtrace) verbose.
In Oracle 12c we got a whole package devoted to PL/SQL call stack: UTL_CALL_STACK. It is a box of bits and requires more than one call to get things but we can retrieve a specific line number with unit_line(). Tim Hall has written a typically fine introduction to the new feature. Find out more.
The other thing to consider is how good program design can resolve this problem. Specifically the Single Responsibility Principle. This is a fundamental guideline of program design: a program unit should do one thing. If we asking the question "which command through this error" it can be a sign that we're violating the SRP.
Let's resign your code so it follows this design principle:
declare
type nt1 is table of t1%rowtype;
type nt2 is table of t2%rowtype;
l_t1 nt1;
l_t2 nt2;
x_t1_ndf exception;
x_t2_ndf exception;
function get_t1 return nt1 is
return_value nt1;
begin
select *
bulk collect into return_value
from t1;
if return_value.count() = 0 then
raise x_t1_ndf;
end if;
return return_value;
end get_t1;
function get_t2 return nt2 is
return_value nt2;
begin
select *
bulk collect into return_value
from t2;
if return_value.count() = 0 then
raise x_t2_ndf;
end if;
return return_value;
end get_t2;
begin
l_t1 := get_t1;
l_t2 := get_t2;
exception
when x_t1_ndf then
dbms_output.put_line('T1 has no data');
when x_t2_ndf then
dbms_output.put_line('T2 has no data');
end;
Obviously more typing than your original code but partly that's because this toy is complete working code, unlike the code you posted. Also in real life these modules would be discrete units, rather than private functions in an anonymous block, and so we could re-use them in multiple other programs.
Also dbms_output.put_line() is not the proper way to handle exceptions, but I've left that because it's what your code does.
There is no built-in that you can use for that.
One way could be by handling the exceptions of the single statements, with something like (pseudo-code):
declare
err varchar2(100);
myException exception;
begin
...
begin
select * from t1;
exception
when others then
err := 'Error in select * from t1: ' || sqlerrm;
raise myException
end;
begin
select * from t2;
exception
when others then
err := 'Error in select * from t2: ' || sqlerrm;
raise myException
end;
...
exception
when myException then
dbms_output.put_line(err);
when others then
dbms_output.put_line('Unhandled exception: ' || sqlerrm);
end;
For something more, this can be very useful.
Using a single exception handler for multiple statements always mask
the statement that caused an error.
Instead, you can use a Local variable(Locator) to track statement execution, as follows:
DECLARE
err_stmt NUMBER:= 1; -- Indicates 1st SELECT statement
BEGIN
SELECT ... -- Statement 1
err_stmt := 2; -- Indicates 2nd SELECT statement
SELECT ... -- Statement 2
EXCEPTION
WHEN OTHERS THEN
dbms_output.put_line(sqlerrm || ' raised from this statement number:' || err_stmt;
END;
Cheers!!
I am using SQL developer to write a procedure.
The objective is to get the name of the drainage system from one table and get the count of how much the drainage system name code appears in another table.
My procedure works, but when I enter an incorrect value, it does not go to the exception section. For example, when I input ‘Mexico River’, this name does not exist in the table. So, I want my exception section to realize that this is an incorrect value being entered.
My question is how do I modify my code, so it can detect incorrect values and go to my exception section.
Below is a snippet of my code:
PROCEDURE number_of_rivers --second procedure with 1 parameter
(input_sysName IN TBLDrainage.DRAINAGE_SYS%TYPE)
is
-- Create a cursor
cursor c_river is
select code, drainage_sys
from TBLDRAINAGE
where DRAINAGE_SYS = input_sysName;
v_rivercount Number;
r_variable c_river %rowtype;
Begin
FOR r_variable in c_river
loop
select count (Drainage_sys) into v_rivercount
from TBLRIVER
where DRAINAGE_SYS = r_variable.code;
DBMS_OUTPUT.PUT_LINE (UPPER(input_sysName) || ' has ' || v_rivercount || ' river(s) in the drainage system.');
end loop;
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE ('Error: Please enter a valid drainage system name');
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE ('Error in finding system');
END ;
The CURSOR..FOR loop has the property of executing zero or more times. It doesn't throw NO_DATA_FOUND.
There are a couple of solutions. One is to include a count inside the loop, and raise an exception afterwards.
l_count := 0;
FOR r_variable in c_river
loop
....
l_count := l_count + 1;
end loop;
if l_count = 0 then
raise NO_DATA_FOUND;
end if;
The other would be to validate the input parameter at the start of your program.
begin
open c_river;
fetch c_river into r_variable;
if c_river%notfound then
raise NO_DATA_FOUND;
else
select count (Drainage_sys)
into v_rivercount
from TBLRIVER
where DRAINAGE_SYS = r_variable.code;
DBMS_OUTPUT.PUT_LINE (UPPER(input_sysName) || ' has ' || v_rivercount || ' river(s) in the drainage system.');
end if;
close c_river;
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE ('Error: Please enter a valid drainage system name');
close c_river;
END ;
In this solution I have removed the loop, because I would expect a look-up on drainage system should be unique and return one record. Please re-instate the loop if your data model isn't like that.
I have retained your names for the cursor and its row variables but you should re-name them. They are used for selecting drainage systems not rivers, and their names ought to reflect that. Discipline in naming things is a useful habit to acquire, as misleading variable names will cause confusion in larger chunks of code.
Also, swallowing exceptions like this is very bad:
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE ('Error in finding system');
Oracle has thousands of error messages: it better to do nothing with the error message than to throw it away.
In our Stored procedures we have the following code towards the very end.
<<SQL_ERROR>>
V_SYS_ERROR_MSG := SUBSTR(SQLERRM, 1, 252);
DBMS_OUTPUT.PUT_LINE('ERROR IN EXECUTION IN PROCEDURE');
DBMS_OUTPUT.PUT_LINE('THE ERROR CODE IS ' || V_SYS_ERROR || '- ' ||
V_SYS_ERROR_MSG);
we have statements like following which call the error block.
IF V_SYS_ERROR <> 0 THEN
GOTO SQL_ERROR;
the DBMS output statements come even when there is no error. How can we avoid this?
I am not recommending this GOTO approach: as others have already said, exceptions are the correct way to handle errors in PL/SQL. But to address your specific question, you could do this:
BEGIN
IF V_SYS_ERROR <> 0 THEN
GOTO SQL_ERROR;
END IF;
GOTO PROC_END;
<<SQL_ERROR>>
V_SYS_ERROR_MSG := SUBSTR(SQLERRM, 1, 252);
DBMS_OUTPUT.PUT_LINE('ERROR IN EXECUTION IN PROCEDURE');
DBMS_OUTPUT.PUT_LINE('THE ERROR CODE IS ' || V_SYS_ERROR || '- ' ||
V_SYS_ERROR_MSG);
<<PROC_END>>
NULL;
END;
Of course, this still involves changing the code, so if you are doing that why not do it properly anyway? i.e.
DECLARE
SQL_ERROR EXCEPTION;
BEGIN
IF V_SYS_ERROR <> 0 THEN
RAISE SQL_ERROR;
END IF;
EXCEPTION
WHEN SQL_ERROR THEN
V_SYS_ERROR_MSG := SUBSTR(SQLERRM, 1, 252);
DBMS_OUTPUT.PUT_LINE('ERROR IN EXECUTION IN PROCEDURE');
DBMS_OUTPUT.PUT_LINE('THE ERROR CODE IS ' || V_SYS_ERROR || '- ' ||
V_SYS_ERROR_MSG);
RAISE;
END;
By the way, DBMS_OUTPUT.PUT_LINE is not suitable for output of error messages in a production application system. Only use it for debugging during development.
you should avoid GOTO statements, they are messy as you have noticed. PL/SQL comes with error handling, you should use the EXCEPTION synthax to deal with errors:
BEGIN
<code goes here>
EXCEPTION
WHEN <exception> THEN
<deal_with_it>
WHEN OTHERS THEN
<log_error>
RAISE;
END;
I don't think you'll get the results you want without using an exception handler. From the PL/SQL User's Guide:
SQLERRM with no argument is useful
only in an exception handler. Outside
a handler, SQLERRM with no argument
always returns the normal, successful
completion message.
So first of all, it is possible that an error is happening but you are seeing a message saying "normal, successful completion" because that is what SQLERRM will always return in this context.
Assuming that's not the case, it sounds like you are simply allowing control to flow from the "normal" code into the "error handler". If the error handler is at the very end of the procedure, then a simple fix would be to add a RETURN statement just before the <> label.
A better fix would be to move the error-handler code to a separate procedure and call that procedure instead of using a GOTO.