How PL/SQL Exceptions Are Raised
Internal exceptions are raised implicitly by the run-time system, as are user-defined exceptions that you have associated with an Oracle error number using EXCEPTION_INIT
. However, other user-defined exceptions must be raised explicitly by RAISE
statements.
Raising Exceptions with the RAISE Statement
PL/SQL blocks and subprograms should raise an exception only when an error makes it undesirable or impossible to finish processing. You can place RAISE
statements for a given exception anywhere within the scope of that exception. In Example 10-6, you alert your PL/SQL block to a user-defined exception named out_of_stock
.DECLARE
out_of_stock EXCEPTION;
number_on_hand NUMBER := 0;
BEGIN
IF number_on_hand < 1 THEN
RAISE out_of_stock; -- raise an exception that we defined
END IF;
EXCEPTION
WHEN out_of_stock THEN
-- handle the error
DBMS_OUTPUT.PUT_LINE('Encountered out-of-stock error.');
END;
/
You can also raise a predefined exception explicitly. That way, an exception handler written for the predefined exception can process other errors, as Example 10-7 shows:DECLARE
acct_type INTEGER := 7;
BEGIN
IF acct_type NOT IN (1, 2, 3) THEN
RAISE INVALID_NUMBER; -- raise predefined exception
END IF;
EXCEPTION
WHEN INVALID_NUMBER THEN
DBMS_OUTPUT.PUT_LINE('HANDLING INVALID INPUT BY ROLLING BACK.');
ROLLBACK;
END;
/
How PL/SQL Exceptions Propagate
When an exception is raised, if PL/SQL cannot find a handler for it in the current block or subprogram, the exception propagates. That is, the exception reproduces itself in successive enclosing blocks until a handler is found or there are no more blocks to search. If no handler is found, PL/SQL returns an unhandled exception error to the host environment.Exceptions cannot propagate across remote procedure calls done through database links.
Figure 10-1, Figure 10-2, and Figure 10-3 illustrate the basic propagation rules.
An exception can propagate beyond its scope, that is, beyond the block in which it was declared, as shown in Example 10-8.
BEGIN
DECLARE ---------- sub-block begins
past_due EXCEPTION;
due_date DATE := trunc(SYSDATE) - 1;
todays_date DATE := trunc(SYSDATE);
BEGIN
IF due_date < todays_date THEN
RAISE past_due;
END IF;
END; ------------- sub-block ends
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
END;
/
Because the block that declares the exception past_due
has no handler for it, the exception propagates to the enclosing block. But the enclosing block cannot reference the name PAST_DUE
, because the scope where it was declared no longer exists. Once the exception name is lost, only an OTHERS
handler can catch the exception. If there is no handler for a user-defined exception, the calling application gets this error:ORA-06510: PL/SQL: unhandled user-defined exception
Reraising a PL/SQL Exception
Sometimes, you want to reraise an exception, that is, handle it locally, then pass it to an enclosing block. For example, you might want to roll back a transaction in the current block, then log the error in an enclosing block.To reraise an exception, use a
RAISE
statement without an exception name, which is allowed only in an exception handler:DECLARE
salary_too_high EXCEPTION;
current_salary NUMBER := 20000;
max_salary NUMBER := 10000;
erroneous_salary NUMBER;
BEGIN
BEGIN ---------- sub-block begins
IF current_salary > max_salary THEN
RAISE salary_too_high; -- raise the exception
END IF;
EXCEPTION
WHEN salary_too_high THEN
-- first step in handling the error
DBMS_OUTPUT.PUT_LINE('Salary ' || erroneous_salary || ' is out of range.');
DBMS_OUTPUT.PUT_LINE('Maximum salary is ' || max_salary || '.');
RAISE; -- reraise the current exception
END; ------------ sub-block ends
EXCEPTION
WHEN salary_too_high THEN
-- handle the error more thoroughly
erroneous_salary := current_salary;
current_salary := max_salary;
DBMS_OUTPUT.PUT_LINE('Revising salary from ' || erroneous_salary ||
' to ' || current_salary || '.');
END;
/
Handling Raised PL/SQL Exceptions
When an exception is raised, normal execution of your PL/SQL block or subprogram stops and control transfers to its exception-handling part, which is formatted as follows:EXCEPTION
WHEN exception1 THEN -- handler for exception1
sequence_of_statements1
WHEN exception2 THEN -- another handler for exception2
sequence_of_statements2
...
WHEN OTHERS THEN -- optional handler for all other errors
sequence_of_statements3
END;
WHEN
clause, which specifies an exception, followed by a sequence of statements to be executed when that exception is raised. These statements complete execution of the block or subprogram; control does not return to where the exception was raised. In other words, you cannot resume processing where you left off.The optional
OTHERS
exception handler, which is always the last handler in a block or subprogram, acts as the handler for all exceptions not named specifically. Thus, a block or subprogram can have only one OTHERS
handler. Use of the OTHERS
handler guarantees that no exception will go unhandled.If you want two or more exceptions to execute the same sequence of statements, list the exception names in the
WHEN
clause, separating them by the keyword OR
, as follows:EXCEPTION
WHEN over_limit OR under_limit OR VALUE_ERROR THEN
-- handle the error
OTHERS
cannot appear in the list of exception names; it must appear by itself. You can have any number of exception handlers, and each handler can associate a list of exceptions with a sequence of statements. However, an exception name can appear only once in the exception-handling part of a PL/SQL block or subprogram.The usual scoping rules for PL/SQL variables apply, so you can reference local and global variables in an exception handler. However, when an exception is raised inside a cursor
FOR
loop, the cursor is closed implicitly before the handler is invoked. Therefore, the values of explicit cursor attributes are not available in the handler.
Exceptions Raised in Declarations
Exceptions can be raised in declarations by faulty initialization expressions. For example, the following declaration raises an exception because the constant credit_limit
cannot store numbers larger than 999:DECLARE
credit_limit CONSTANT NUMBER(3) := 5000; -- raises an error
BEGIN
NULL;
EXCEPTION
WHEN OTHERS THEN
-- Cannot catch the exception. This handler is never called.
DBMS_OUTPUT.PUT_LINE('Can''t handle an exception in a declaration.');
END;
/
Handlers in the current block cannot catch the raised exception because an exception raised in a declaration propagates immediately to the enclosing block.
Handling Exceptions Raised in Handlers
When an exception occurs within an exception handler, that same handler cannot catch the exception. An exception raised inside a handler propagates immediately to the enclosing block, which is searched to find a handler for this new exception. From there on, the exception propagates normally. For example:EXCEPTION
WHEN INVALID_NUMBER THEN
INSERT INTO ... -- might raise DUP_VAL_ON_INDEX
WHEN DUP_VAL_ON_INDEX THEN ... -- cannot catch the exception
END;
Branching to or from an Exception Handler
A GOTO
statement can branch from an exception handler into an enclosing block.A
GOTO
statement cannot branch into an exception handler, or from an exception handler into the current block.
Retrieving the Error Code and Error Message: SQLCODE and SQLERRM
In an exception handler, you can use the built-in functions SQLCODE
and SQLERRM
to find out which error occurred and to get the associated error message. For internal exceptions, SQLCODE
returns the number of the Oracle error. The number that SQLCODE
returns is negative unless the Oracle error is no data found
, in which case SQLCODE
returns +100. SQLERRM
returns the corresponding error message. The message begins with the Oracle error code.For user-defined exceptions,
SQLCODE
returns +1 and SQLERRM
returns the message User-Defined Exception
unless you used the pragma EXCEPTION_INIT
to associate the exception name with an Oracle error number, in which case SQLCODE
returns that error number and SQLERRM
returns the corresponding error message. The maximum length of an Oracle error message is 512 characters including the error code, nested messages, and message inserts such as table and column names.If no exception has been raised,
SQLCODE
returns zero and SQLERRM
returns the message: ORA-0000: normal, successful completion
.You can pass an error number to
SQLERRM
, in which case SQLERRM
returns the message associated with that error number. Make sure you pass negative error numbers to SQLERRM
.Passing a positive number to
SQLERRM
always returns the message user-defined exception
unless you pass +100
, in which case SQLERRM
returns the message no data found
. Passing a zero to SQLERRM
always returns the message normal, successful completion
.You cannot use
SQLCODE
or SQLERRM
directly in a SQL statement. Instead, you must assign their values to local variables, then use the variables in the SQL statement, as shown in Example 10-11.CREATE TABLE errors (code NUMBER, message VARCHAR2(64), happened TIMESTAMP);
DECLARE
name employees.last_name%TYPE;
v_code NUMBER;
v_errm VARCHAR2(64);
BEGIN
SELECT last_name INTO name FROM employees WHERE employee_id = -1;
EXCEPTION
WHEN OTHERS THEN
v_code := SQLCODE;
v_errm := SUBSTR(SQLERRM, 1 , 64);
DBMS_OUTPUT.PUT_LINE('Error code ' || v_code || ': ' || v_errm);
-- Normally we would call another procedure, declared with PRAGMA
-- AUTONOMOUS_TRANSACTION, to insert information about errors.
INSERT INTO errors VALUES (v_code, v_errm, SYSTIMESTAMP);
END;
/
The string function SUBSTR
ensures that a VALUE_ERROR
exception (for truncation) is not raised when you assign the value of SQLERRM
to err_msg
. The functions SQLCODE
and SQLERRM
are especially useful in the OTHERS
exception handler because they tell you which internal exception was raised.When using pragma
RESTRICT_REFERENCES
to assert the purity of a stored function, you cannot specify the constraints WNPS
and RNPS
if the function calls SQLCODE
or SQLERRM
.
Catching Unhandled Exceptions
Remember, if it cannot find a handler for a raised exception, PL/SQL returns an unhandled exception error to the host environment, which determines the outcome. For example, in the Oracle Precompilers environment, any database changes made by a failed SQL statement or PL/SQL block are rolled back.Unhandled exceptions can also affect subprograms. If you exit a subprogram successfully, PL/SQL assigns values to
OUT
parameters. However, if you exit with an unhandled exception, PL/SQL does not assign values to OUT
parameters (unless they are NOCOPY
parameters). Also, if a stored subprogram fails with an unhandled exception, PL/SQL does not roll back database work done by the subprogram.You can avoid unhandled exceptions by coding an
OTHERS
handler at the topmost level of every PL/SQL program.
Tips for Handling PL /SQL Errors
In this section, you learn techniques that increase flexibility.
Continuing after an Exception Is Raised
An exception handler lets you recover from an otherwise fatal error before exiting a block. But when the handler completes, the block is terminated. You cannot return to the current block from an exception handler. In the following example, if the SELECT
INTO
statement raises ZERO_DIVIDE
, you cannot resume with the INSERT
statement:CREATE TABLE employees_temp AS
SELECT employee_id, salary, commission_pct FROM employees;
DECLARE
sal_calc NUMBER(8,2);
BEGIN
INSERT INTO employees_temp VALUES (301, 2500, 0);
SELECT salary / commission_pct INTO sal_calc FROM employees_temp
WHERE employee_id = 301;
INSERT INTO employees_temp VALUES (302, sal_calc/100, .1);
EXCEPTION
WHEN ZERO_DIVIDE THEN
NULL;
END;
/
You can still handle an exception for a statement, then continue with the next statement. Place the statement in its own sub-block with its own exception handlers. If an error occurs in the sub-block, a local handler can catch the exception. When the sub-block ends, the enclosing block continues to execute at the point where the sub-block ends, as shown in Example 10-12.DECLARE
sal_calc NUMBER(8,2);
BEGIN
INSERT INTO employees_temp VALUES (303, 2500, 0);
BEGIN -- sub-block begins
SELECT salary / commission_pct INTO sal_calc FROM employees_temp
WHERE employee_id = 301;
EXCEPTION
WHEN ZERO_DIVIDE THEN
sal_calc := 2500;
END; -- sub-block ends
INSERT INTO employees_temp VALUES (304, sal_calc/100, .1);
EXCEPTION
WHEN ZERO_DIVIDE THEN
NULL;
END;
/
In this example, if the SELECT
INTO
statement raises a ZERO_DIVIDE
exception, the local handler catches it and sets sal_calc
to 2500. Execution of the handler is complete, so the sub-block terminates, and execution continues with the INSERT
statement. See also Example 5-38, "Collection Exceptions".You can also perform a sequence of DML operations where some might fail, and process the exceptions only after the entire operation is complete, as described in "Handling FORALL Exceptions with the %BULK_EXCEPTIONS Attribute".
Retrying a Transaction
After an exception is raised, rather than abandon your transaction, you might want to retry it. The technique is:
1. Encase the transaction in a sub-block.
2. Place the sub-block inside a loop that repeats the transaction.
3. Before starting the transaction, mark a savepoint. If the transaction succeeds, commit, then exit from the loop. If the transaction fails, control transfers to the exception handler, where you roll back to the savepoint undoing any changes, then try to fix the problem.
In Example 10-13, the INSERT
statement might raise an exception because of a duplicate value in a unique column. In that case, we change the value that needs to be unique and continue with the next loop iteration. If the INSERT succeeds, we exit from the loop immediately. With this technique, you should use a FOR
or WHILE
loop to limit the number of attempts.CREATE TABLE results ( res_name VARCHAR(20), res_answer VARCHAR2(3) );
CREATE UNIQUE INDEX res_name_ix ON results (res_name);
INSERT INTO results VALUES ('SMYTHE', 'YES');
INSERT INTO results VALUES ('JONES', 'NO');
DECLARE
name VARCHAR2(20) := 'SMYTHE';
answer VARCHAR2(3) := 'NO';
suffix NUMBER := 1;
BEGIN
FOR i IN 1..5 LOOP -- try 5 times
BEGIN -- sub-block begins
SAVEPOINT start_transaction; -- mark a savepoint
/* Remove rows from a table of survey results. */
DELETE FROM results WHERE res_answer = 'NO';
/* Add a survey respondent's name and answers. */
INSERT INTO results VALUES (name, answer);
-- raises DUP_VAL_ON_INDEX if two respondents have the same name
COMMIT;
EXIT;
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
ROLLBACK TO start_transaction; -- undo changes
suffix := suffix + 1; -- try to fix problem
name := name || TO_CHAR(suffix);
END; -- sub-block ends
END LOOP ;
END;
/
Using Locator Variables to Identify Exception Locations
Using one exception handler for a sequence of statements, such as INSERT
, DELETE
, or UPDATE
statements, can mask the statement that caused an error. If you need to know which statement failed, you can use a locator variable
:CREATE OR REPLACE PROCEDURE loc_var AS
stmt_no NUMBER;
name VARCHAR2(100);
BEGIN
stmt_no := 1; -- designates 1st SELECT statement
SELECT table_name INTO name FROM user_tables WHERE table_name LIKE 'ABC%';
stmt_no := 2; -- designates 2nd SELECT statement
SELECT table_name INTO name FROM user_tables WHERE table_name LIKE 'XYZ%';
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('Table name not found in query ' || stmt_no);
END;
/
CALL loc_var();
Overview of PL/SQL Compile-Time Warnings
To make your programs more robust and avoid problems at run time, you can turn on checking for certain warning conditions. These conditions are not serious enough to produce an error and keep you from compiling a subprogram. They might point out something in the subprogram that produces an undefined result or might create a performance problem.To work with PL/SQL warning messages, you use the
PLSQL_WARNINGS
initialization parameter, the DBMS_WARNING
package, and the USER/DBA/ALL_PLSQL_OBJECT_SETTINGS
views.
PL/SQL Warning Categories
PL/SQL warning messages are divided into categories, so that you can suppress or display groups of similar warnings during compilation. The categories are:
·
SEVERE
: Messages for conditions that might cause unexpected behavior or wrong results, such as aliasing problems with parameters.
·
PERFORMANCE
: Messages for conditions that might cause performance problems, such as passing a VARCHAR2
value to a NUMBER
column in an INSERT
statement.
·
The keyword INFORMATIONAL
: Messages for conditions that do not have an effect on performance or correctness, but that you might want to change to make the code more maintainable, such as unreachable code that can never be executed.All
is a shorthand way to refer to all warning messages.You can also treat particular messages as errors instead of warnings. For example, if you know that the warning message
PLW-05003
represents a serious problem in your code, including 'ERROR:05003'
in the PLSQL_WARNINGS
setting makes that condition trigger an error message (PLS_05003
) instead of a warning message. An error message causes the compilation to fail.
Controlling PL/SQL Warning Messages
To let the database issue warning messages during PL/SQL compilation, you set the initialization parameter PLSQL_WARNINGS
. You can enable and disable entire categories of warnings (ALL
, SEVERE
, INFORMATIONAL
, PERFORMANCE
), enable and disable specific message numbers, and make the database treat certain warnings as compilation errors so that those conditions must be corrected.This parameter can be set at the system level or the session level. You can also set it for a single compilation by including it as part of the
ALTER PROCEDURE
... COMPILE
statement. You might turn on all warnings during development, turn off all warnings when deploying for production, or turn on some warnings when working on a particular subprogram where you are concerned with some aspect, such as unnecessary code or performance.-- To focus on one aspect
ALTER SESSION SET PLSQL_WARNINGS='ENABLE:PERFORMANCE';
-- Recompile with extra checking
ALTER PROCEDURE loc_var COMPILE PLSQL_WARNINGS='ENABLE:PERFORMANCE'
REUSE SETTINGS;
-- To turn off all warnings
ALTER SESSION SET PLSQL_WARNINGS='DISABLE:ALL';
-- Display 'severe' warnings, don't want 'performance' warnings, and
-- want PLW-06002 warnings to produce errors that halt compilation
ALTER SESSION SET PLSQL_WARNINGS='ENABLE:SEVERE', 'DISABLE:PERFORMANCE',
'ERROR:06002';
-- For debugging during development
ALTER SESSION SET PLSQL_WARNINGS='ENABLE:ALL';
Warning messages can be issued during compilation of PL/SQL subprograms; anonymous blocks do not produce any warnings.The settings for the
PLSQL_WARNINGS
parameter are stored along with each compiled subprogram. If you recompile the subprogram with a CREATE OR REPLACE
statement, the current settings for that session are used. If you recompile the subprogram with an ALTER
... COMPILE
statement, the current session setting might be used, or the original setting that was stored with the subprogram, depending on whether you include the REUSE SETTINGS
clause in the statement. For more information, see ALTER
FUNCTION
, ALTER
PACKAGE
, and ALTER
PROCEDURE
in Oracle Database SQL Reference.To see any warnings generated during compilation, you use the SQL*Plus
SHOW ERRORS
command or query the USER_ERRORS
data dictionary view. PL/SQL warning messages all use the prefix PLW
.
Using the DBMS_WARNING Package
If you are writing a development environment that compiles PL/SQL subprograms, you can control PL/SQL warning messages by calling subprograms in the DBMS_WARNING
package. You might also use this package when compiling a complex application, made up of several nested SQL*Plus scripts, where different warning settings apply to different subprograms. You can save the current state of the PLSQL_WARNINGS
parameter with one call to the package, change the parameter to compile a particular set of subprograms, then restore the original parameter value.For example, Example 10-16 is a procedure with unnecessary code that could be removed. It could represent a mistake, or it could be intentionally hidden by a debug flag, so you might or might not want a warning message for it.
-- When warnings disabled, the following procedure compiles with no warnings
CREATE OR REPLACE PROCEDURE unreachable_code AS
x CONSTANT BOOLEAN := TRUE;
BEGIN
IF x THEN
DBMS_OUTPUT.PUT_LINE('TRUE');
ELSE
DBMS_OUTPUT.PUT_LINE('FALSE');
END IF;
END unreachable_code;
/
-- enable all warning messages for this session
CALL DBMS_WARNING.set_warning_setting_string('ENABLE:ALL' ,'SESSION');
-- Check the current warning setting
SELECT DBMS_WARNING.get_warning_setting_string() FROM DUAL;
-- Recompile the procedure and a warning about unreachable code displays
ALTER PROCEDURE unreachable_code COMPILE;
SHOW ERRORS;
In Example 10-16, you could have used the following ALTER
PROCEDURE
without the call to DBMS_WARNINGS.set_warning_setting_string
:ALTER PROCEDURE unreachable_code COMPILE
PLSQL_WARNINGS = 'ENABLE:ALL' REUSE SETTINGS;
Comments
Post a Comment