PL/SQL exception handling separates error logic from business logic using an EXCEPTION block
Three categories: predefined (NO_DATA_FOUND), non-predefined (mapped via PRAGMA), and user-defined (business rule violations)
Performance: unhandled exceptions cause implicit ROLLBACK, which is slower than explicit error responses
Production insight: WHEN OTHERS THEN NULL silently kills errors — you'll debug blind for hours
Biggest mistake: Using SELECT INTO without a NO_DATA_FOUND handler in batch jobs — whole transaction rolls back on missing rows
✦ Definition~90s read
What is PL/SQL Exception Handling?
PL/SQL Exception Handling is a core feature of PL/SQL. It was designed to solve a specific problem: unhandled errors causing transaction failure and messy stack traces for the end-user. It exists to provide a centralized 'EXCEPTION' block at the end of your code, keeping your main execution path clean.
★
Think of PL/SQL Exception Handling as a powerful tool in your developer toolkit.
There are three categories: Predefined (like NO_DATA_FOUND), Non-predefined (standard Oracle errors mapped to names), and User-defined (business rule violations). This architecture allows for graceful recovery, customized error logging, and consistent transaction management via ROLLBACK.
Plain-English First
Think of PL/SQL Exception Handling as a powerful tool in your developer toolkit. Once you understand what it does and when to reach for it, everything clicks into place. Imagine you are operating a complex manufacturing assembly line. Without exception handling, a single jammed gear causes the entire factory to explode. With exception handling, the system detects the jam, safely pauses that specific machine, logs the error for the mechanic, and allows the rest of the factory to keep running or shuts down gracefully without losing progress.
PL/SQL Exception Handling is a fundamental concept in Database development. In the world of Oracle programming, an exception is an error condition that occurs during the execution of a block. Effective handling ensures that your application remains stable even when the database encounters unexpected data or system issues.
In this guide we'll break down exactly what PL/SQL Exception Handling is, why it was designed to separate error logic from business logic, and how to use it correctly in real projects.
By the end you'll have both the conceptual understanding and practical code examples to use PL/SQL Exception Handling with confidence.
What Is PL/SQL Exception Handling and Why Does It Exist?
PL/SQL Exception Handling is a core feature of PL/SQL. It was designed to solve a specific problem: unhandled errors causing transaction failure and messy stack traces for the end-user. It exists to provide a centralized 'EXCEPTION' block at the end of your code, keeping your main execution path clean. There are three categories: Predefined (like NO_DATA_FOUND), Non-predefined (standard Oracle errors mapped to names), and User-defined (business rule violations). This architecture allows for graceful recovery, customized error logging, and consistent transaction management via ROLLBACK.
ForgeExceptionDemo.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
-- io.thecodeforge: Standard Exception Handling PatternDECLARE
v_project_name forge_projects.name%TYPE;
e_invalid_status EXCEPTION; -- User-defined exceptionBEGIN-- Standard query that might failSELECT name INTO v_project_name
FROM forge_projects
WHERE project_id = 999;
-- Business logic checkIF v_project_name ISNULLTHENRAISE e_invalid_status;
ENDIF;
EXCEPTIONWHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('Forge Error: Project record not found.');
WHEN e_invalid_status THEN
DBMS_OUTPUT.PUT_LINE('Forge Error: Project status is corrupted.');
WHENOTHERSTHEN-- Catch-all for unexpected issues
DBMS_OUTPUT.PUT_LINE('Unexpected Error Code: ' || SQLCODE || ' - ' || SQLERRM);
RAISE; -- Re-propagate the error after loggingEND;
Output
Forge Error: Project record not found.
Key Insight:
The most important thing to understand about PL/SQL Exception Handling is the problem it was designed to solve. Always ask 'why does this exist?' before asking 'how do I use it?' It exists to separate your 'happy path' code from your 'unhappy path' error logic, making both easier to maintain.
Production Insight
A team once used WHEN OTHERS THEN NULL in a stored procedure that processed 100k rows per hour.
The error was a foreign key violation they never saw — the transaction silently rolled back each row.
Hours of data went missing before anyone noticed.
Rule: always log SQLCODE and SQLERRM. Never catch exceptions you don't understand.
Key Takeaway
Separate error logic from business logic.
Use WHEN OTHERS only for logging and re-raise.
Catch specific exceptions where they occur.
thecodeforge.io
PL/SQL Exception Handling Flow
Plsql Exception Handling
Types of Exceptions: Predefined, Non-Predefined, and User-Defined
Oracle provides a set of predefined exceptions like NO_DATA_FOUND (ORA-01403), TOO_MANY_ROWS (ORA-01422), DUP_VAL_ON_INDEX (ORA-00001), and others. These are automatically raised when the corresponding Oracle error occurs. Non-predefined exceptions cover standard Oracle errors that don't have a built-in name — you map them using PRAGMA EXCEPTION_INIT. User-defined exceptions are declared with the EXCEPTION keyword and raised explicitly with RAISE or RAISE_APPLICATION_ERROR. The choice depends on the error's source: predefined for common Oracle errors, non-predefined for specific ORA codes you want a name for, user-defined for your application's business rules.
ExceptionTypes.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- io.thecodeforge: Mapping a non-predefined Oracle errorDECLARE
e_fk_violation EXCEPTION;
PRAGMA EXCEPTION_INIT(e_fk_violation, -2292); -- ORA-02292: child record foundBEGINDELETEFROM forge_projects WHERE project_id = 100;
EXCEPTIONWHEN e_fk_violation THEN
DBMS_OUTPUT.PUT_LINE('Cannot delete: foreign key constraint violated');
-- Optionally re-raise with custom message
RAISE_APPLICATION_ERROR(-20002, 'Cannot delete active project');
WHENOTHERSTHEN
DBMS_OUTPUT.PUT_LINE('Unexpected: ' || SQLERRM);
RAISE;
END;
Output
Cannot delete: foreign key constraint violated
PRAGMA EXCEPTION_INIT Placement
The PRAGMA must appear in the declarative section after the exception name declaration but before the BEGIN keyword. It associates an Oracle error number with your custom exception name.
Production Insight
Using PRAGMA EXCEPTION_INIT for -2292 is a classic pattern. But if you forget to handle it and rely on WHEN OTHERS, you might accidentally log but not re-raise, silently swallowing foreign key violations.
Always re-raise expected non-predefined exceptions after logging.
You don't want to lose referential integrity reports because an error was silently consumed.
Key Takeaway
Predefined for common Oracle errors.
Non-predefined for specific ORA codes you want named.
User-defined for business logic violations.
All three should be explicitly handled — never let them fall through to WHEN OTHERS.
Exception Propagation and Scope
Exceptions propagate from inner blocks to outer blocks until caught or until they reach the outermost block and become unhandled, causing the program to terminate. If an exception is raised in the declarative section (e.g., a variable initialisation fails), it cannot be caught by the same block's EXCEPTION section — it propagates immediately to the enclosing block. Understanding scope is critical: each BEGIN-END block has its own exception section. An exception raised in an inner block that is not handled there will propagate to the outer block's EXCEPTION section. You can use nested blocks to isolate errors and continue processing while still maintaining transactional integrity.
NestedScope.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- io.thecodeforge: Nested block isolationDECLARE
v_total NUMBER := 0;
BEGINFOR rec IN (SELECT id FROM forge_tasks WHERE status = 'ACTIVE') LOOPBEGIN-- Inner block isolates this row's processingUPDATE forge_tasks SET processed_date = SYSDATEWHERE id = rec.id;
v_total := v_total + 1;
EXCEPTIONWHENOTHERSTHEN
DBMS_OUTPUT.PUT_LINE('Skipping task ' || rec.id || ' due to: ' || SQLERRM);
-- Continue with next record; transaction not rolled backEND;
ENDLOOP;
DBMS_OUTPUT.PUT_LINE('Processed ' || v_total || ' tasks successfully.');
EXCEPTIONWHENOTHERSTHENROLLBACK;
RAISE;
END;
Output
Skipping task 42 due to ORA-01403: no data found
Processed 99 tasks successfully.
Propagation Pitfall: Declarative Section Errors
An exception raised in the DECLARE section (e.g., constraint violation on a variable) cannot be caught in the same block's EXCEPTION section. It propagates to the enclosing block. Always initialise variables with safe values or use default assignments to avoid this.
Production Insight
In a real payroll system, a developer placed a SELECT INTO inside a function's declaration to initialise a constant. When the lookup failed, the function crashed with an unhandled NO_DATA_FOUND, aborting the entire payroll run.
The fix: move the initialisation into the executable section with its own handler.
Rule: never put runtime-dependent logic in the declarative section.
Key Takeaway
Inner blocks catch and isolate errors.
Propagation goes up until handled.
Declaration section errors cannot be caught locally.
Use nested blocks for row-level fault tolerance.
RAISE vs RAISE_APPLICATION_ERROR: When to Use Each
RAISE is used to re-raise the current exception or explicitly raise a user-defined exception declared with EXCEPTION. It preserves the original error stack. RAISE_APPLICATION_ERROR is a built-in procedure that raises a user-defined Oracle error with a custom message and optional flag to keep the error stack. Use RAISE_APPLICATION_ERROR when you want to raise a new error with a specific code (between -20000 and -20999) and message, especially when the caller expects a numeric error code. For simple business rule violations, a user-defined exception with RAISE is cleaner. For errors that need to be caught by external applications (like Java via JDBC), RAISE_APPLICATION_ERROR is better because it provides a standard Oracle error.
Use RAISE for internal propagation within the same PL/SQL unit. Use RAISE_APPLICATION_ERROR when the error must be communicated to a calling program (e.g., a Java stored procedure call) with a standard Oracle error number.
Production Insight
A batch process used RAISE for all business errors. The Java caller that invoked the PL/SQL via JDBC could only see ORA-06510 (unhandled user-defined exception) with no meaningful message.
Switching to RAISE_APPLICATION_ERROR with error codes -20100 to -20199 gave the Java layer actionable error numbers.
Case in point: your error propagation strategy must match the calling context.
Key Takeaway
RAISE preserves the stack, RAISE_APPLICATION_ERROR gives a new ORA code.
Use RAISE within modules, RAISE_APPLICATION_ERROR for API boundaries.
Always include context in the message.
Common Mistakes and How to Avoid Them
When learning PL/SQL Exception Handling, most developers hit the same set of gotchas. The most dangerous is the 'Silent Failure'—using WHEN OTHERS THEN NULL; which swallows the error and makes debugging impossible. Another is failing to ROLLBACK a failed transaction, leaving the database in an inconsistent state.
Knowing these in advance saves hours of debugging production outages and prevents data corruption.
CommonMistakes.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- io.thecodeforge: Handling exceptions in nested blocksBEGIN-- Outer BlockBEGIN-- Inner Block: isolate failures to keep the outer process aliveINSERTINTOforge_logs (msg) VALUES ('Starting Task');
EXCEPTIONWHENOTHERSTHEN
DBMS_OUTPUT.PUT_LINE('Log failed, but continuing main task.');
END;
-- Main business logic continues hereUPDATE forge_stats SET count = count + 1;
END;
Output
// Task proceeds even if logging fails due to local scoping.
Watch Out:
The most common mistake with PL/SQL Exception Handling is using it when a simpler alternative would work better. Do not use exceptions for expected control flow—for example, if a record might or might not exist, sometimes a cursor is more efficient than a SELECT INTO that throws NO_DATA_FOUND.
Production Insight
A system stored procedure used SELECT INTO for every row in a cursor loop. When even one row was missing (due to a DELETE elsewhere), the entire batch failed with NO_DATA_FOUND.
The fix: switch to a cursor loop with explicit FETCH and %NOTFOUND check to avoid exception overhead.
Batch jobs should never rely on exceptions for normal absent data.
Key Takeaway
Do not use exceptions for normal control flow.
Use conditional checks (IF, cursors) when absence is expected.
Always log exception context in production.
Best Practices for Production-Ready Exception Handling
Beyond the basics, robust exception handling in production requires logging, transaction management, and consistent propagation. Create a central error logging procedure that captures SQLCODE, SQLERRM, backtrace (DBMS_UTILITY.FORMAT_ERROR_BACKTRACE), and timestamp. Use autonomous transactions for error logging so that log inserts survive a rollback. Always re-raise exceptions after logging unless the error is truly recoverable. For batch processing, use SAVEPOINT and ROLLBACK TO SAVEPOINT to isolate failed rows. Handle COM (Communication) and TIMEOUT_ON_RESOURCE separately from application errors.
Autonomous transaction ensures the log persists even if the outer transaction rolls back.
DBMS_UTILITY.FORMAT_ERROR_BACKTRACE gives the exact line number — essential for debugging.
Always re-raise after logging unless the error is expected and recoverable.
Use WHEN OTHERS THEN log_error(...); RAISE; as a universal pattern.
Production Insight
A financial reconciliation stored procedure used a single WHEN OTHERS that called ROLLBACK and returned a success code. The logs showed 'success' but the data was missing.
The fix: replace with central logging and explicit re-raise so the caller knows the procedure failed.
Never mask errors. Your monitoring tools need real exception signals.
Key Takeaway
Log with autonomous transactions.
Re-raise after logging.
Use SAVEPOINT for batch isolations.
Never return success codes from failed procedures.
The Execution Flow: What Happens When an Exception Is Raised?
You need to understand the sequence, not just the syntax. When an error occurs in the executable section of a PL/SQL block, control immediately jumps to the EXCEPTION section. The remaining code in the executable block is skipped entirely. No rollback happens automatically. Transaction control is your job. If the exception is handled, the block completes normally. If it's not handled, the exception propagates up to the calling block or client. This is why you must decide where to catch and where to let it bubble. A common production mistake: assuming the block resumes after handling. It does not. The executable block is aborted. Plan your logic accordingly.
employee_update.sqlPLSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// io.thecodeforge
DECLARE
v_emp_id employees.emp_id%TYPE := 9999;
v_salary employees.salary%TYPE;
BEGINSELECT salary INTO v_salary
FROM employees
WHERE emp_id = v_emp_id;
UPDATE employees
SET salary = v_salary * 1.1WHERE emp_id = v_emp_id;
DBMS_OUTPUT.PUT_LINE('Salary updated successfully.');
EXCEPTIONWHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('Employee ID not found. No rows updated.');
WHENOTHERSTHEN
DBMS_OUTPUT.PUT_LINE('Error: ' || SQLERRM);
ROLLBACK;
RAISE;
END;
/
Output
Employee ID not found. No rows updated.
-- Note: No UPDATE executed. SELECT aborted the block.
Production Trap:
Many devs forget that an exception in the SELECT bypasses the UPDATE. Always check DML in the same block—if the SELECT fails, you just skipped an UPDATE that should have been atomic. Wrap them in a SAVEPOINT and handle with care.
Key Takeaway
An exception in the executable section skips all remaining code. Handle early, or propagate deliberately.
Logging Errors Without Losing the Stack: SQLCODE and SQLERRM
You cannot fix what you do not see. The implicit variables SQLCODE and SQLERRM give you the numeric error code and the full error message from the last raised exception. Use them. Always log them in your exception handler. Never rely on DBMS_OUTPUT.PUT_LINE alone; that goes to the client console and disappears. In production, write to an error log table. Wrap the logging in an autonomous transaction (PRAGMA AUTONOMOUS_TRANSACTION) so the log persists even if the parent transaction rolls back. Why? Because if you do not log, you are blind during postmortem. The junior who skips this will spend hours guessing what went wrong. You will spend minutes reading the log.
-1476 ORA-01476: divisor is zero DIVISION_CALC 09-OCT-2025 14:32:18
Senior Engineer Note:
Autonomous transactions commit independently. Use them for logging only—do not mix application logic inside them. Each autonomous transaction creates a separate session, so there is a small overhead. Keep it lean.
Key Takeaway
Log SQLCODE and SQLERRM into a persistent table using an autonomous transaction. Never rely on client-side output for production debugging.
● Production incidentPOST-MORTEMseverity: high
Silent Data Loss in a Nightly Batch Job
Symptom
Nightly batch completes without errors but client reports show zero data. No ORA- errors logged anywhere.
Assumption
The team assumed an exception handler in the outer block would catch all errors. But the SELECT INTO inside a cursor loop had no local handler, and the exception propagated to the block's EXCEPTION section, which logged it but did not re-raise. The transaction rolled back silently.
Root cause
A missing foreign key record caused SELECT INTO to raise NO_DATA_FOUND inside a FOR loop. The exception was caught by WHEN OTHERS in the outer block, which called ROLLBACK via an autonomous transaction log. The rollback wiped all prior inserts in that batch iteration, but the loop continued because the exception was swallowed without re-raise. Later commits never persisted the data for that client.
Fix
Add local exception handlers inside loops: encapsulate each row operation in a sub-block with WHEN NO_DATA_FOUND THEN NULL (to skip missing dependencies) and log the row ID. Never rely on outer WHEN OTHERS to handle expected exceptions like NO_DATA_FOUND. Use SAVEPOINT before each critical operation so you can rollback to a known point without losing the entire batch.
Key lesson
Always handle NO_DATA_FOUND explicitly where it occurs — don't let it bubble up blindly.
Never use ROLLBACK inside an exception handler in a loop unless you also re-raise or exit the loop.
Use SAVEPOINTs inside iterative processing to isolate failures to a single row.
Test batch jobs with intentionally missing data to verify exception scope behaviour.
Production debug guideSymptom-based actions for common exception failures3 entries
Symptom · 01
ORA-01403: No data found in a SELECT INTO, but the data exists in the table
→
Fix
Check if the query uses a WHERE clause that accidentally filters out the row due to NULL comparisons. Run the SELECT as a cursor loop to confirm existence. Add column-level logging with DBMS_OUTPUT.PUT_LINE before the SELECT.
Symptom · 02
WHEN OTHERS handler catches everything, but you can't tell which error occurred
→
Fix
Replace WHEN OTHERS THEN NULL with WHEN OTHERS THEN log_error(SQLCODE, SQLERRM) — store the code and message in a custom error log table. Then re-raise using RAISE; to propagate the original exception.
Symptom · 03
Exception from an inner block appears to be 'swallowed' but the outer block still fails
→
Fix
Verify that the inner block has its own EXCEPTION section and that it does not contain a RAISE without a handler. If the inner block raises an exception not handled locally, it propagates to the outer block. Use nested block scope diagrams to trace propagation.
★ Quick Debug Cheat Sheet: PL/SQL ExceptionsImmediate commands and fixes for common exception issues in Oracle PL/SQL
ORA-06512: unhandled exception with no line number context−
Immediate action
Enable DBMS_OUTPUT and add out-of-the-box error logging in the exception block
Use RAISE_APPLICATION_ERROR for API boundaries, RAISE for internal propagation.
6
Central error logging with autonomous transactions ensures errors survive rollbacks.
Common mistakes to avoid
3 patterns
×
Using 'WHEN OTHERS THEN NULL'
Symptom
Errors are silently swallowed. Production issues go undetected for hours or days. Reports show missing data with no error logs.
Fix
Replace with WHEN OTHERS THEN log_error(SQLCODE, SQLERRM); RAISE;. Use an autonomous transaction logging procedure to preserve the log despite rollback.
×
Not understanding exception propagation from the DECLARE section
Symptom
A variable initialisation that performs a SELECT INTO fails. The exception propagates outside the block, causing the entire procedure to abort with no local handler.
Fix
Never put runtime-dependent logic in the DECLARE section. Initialise variables in the executable section within a nested block with its own exception handler.
×
Ignoring error handling in loops — failing to isolate row-level exceptions
Symptom
A single bad row causes the entire loop to stop processing remaining rows. All prior work in that iteration is rolled back.
Fix
Wrap each loop iteration in a BEGIN-END sub-block with local exception handler. Use SAVEPOINT before each iteration to allow rollback of that row only. Log and continue.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01JUNIOR
What is the difference between SQLCODE and SQLERRM in an exception block...
Q02SENIOR
How do you associate a user-defined name with a standard Oracle error co...
Q03SENIOR
What happens to a transaction if an exception is raised but not caught?
Q04SENIOR
Explain the difference between RAISE and RAISE_APPLICATION_ERROR.
Q01 of 04JUNIOR
What is the difference between SQLCODE and SQLERRM in an exception block?
ANSWER
SQLCODE returns the numeric error code (e.g., -1403 for NO_DATA_FOUND). SQLERRM returns the corresponding error message string (e.g., 'ORA-01403: no data found'). Use SQLCODE for switch logic or logging numeric codes; use SQLERRM for human-readable messages. Both are only valid inside an exception handler. Outside, SQLCODE defaults to 0 and SQLERRM returns 'ORA-0000: normal, successful completion'.
Q02 of 04SENIOR
How do you associate a user-defined name with a standard Oracle error code using PRAGMA EXCEPTION_INIT?
ANSWER
First declare a user-defined exception: e_fk_violation EXCEPTION; Then use PRAGMA EXCEPTION_INIT(e_fk_violation, -2292); This maps Oracle error ORA-02292 to your exception name. The PRAGMA must appear in the declarative section after the exception declaration and before the BEGIN. You can then handle it in the EXCEPTION section: WHEN e_fk_violation THEN ...
Q03 of 04SENIOR
What happens to a transaction if an exception is raised but not caught?
ANSWER
If an exception propagates out of the outermost block without being caught, the PL/SQL block terminates and control returns to the caller. The current transaction is implicitly rolled back only if the error is an unhandled exception that causes the whole call to abort. In Oracle, PL/SQL blocks by default do not automatically rollback on exception unless the exception is unhandled and propagates to the calling environment (e.g., SQL*Plus or a Java app). It is a common misconception that an unhandled exception always rolls back. In reality, changes made in the block may remain uncommitted but not rolled back until the session ends or an explicit ROLLBACK is issued. Always explicitly handle rollback in your exception handlers.
Q04 of 04SENIOR
Explain the difference between RAISE and RAISE_APPLICATION_ERROR.
ANSWER
RAISE is a PL/SQL keyword that re-raises the current exception (when used without an exception name in a handler) or raises a specific user-defined exception. It preserves the original error stack. RAISE_APPLICATION_ERROR is a built-in procedure that raises a new Oracle error with a code between -20000 and -20999 and a custom message. It optionally accepts a third parameter (keep_error_stack) that, when TRUE, preserves the previous error stack. Use RAISE for internal propagation; use RAISE_APPLICATION_ERROR to communicate errors to external callers with a standard error number.
01
What is the difference between SQLCODE and SQLERRM in an exception block?
JUNIOR
02
How do you associate a user-defined name with a standard Oracle error code using PRAGMA EXCEPTION_INIT?
SENIOR
03
What happens to a transaction if an exception is raised but not caught?
SENIOR
04
Explain the difference between RAISE and RAISE_APPLICATION_ERROR.
SENIOR
FAQ · 3 QUESTIONS
Frequently Asked Questions
01
Can I use multiple WHEN clauses for the same exception?
No. Once a WHEN clause matches an exception, control jumps to that handler and then exits the EXCEPTION section. You cannot chain multiple WHEN for the same exception. If you need multiple actions, call a procedure from a single WHEN handler.
Was this helpful?
02
What is the maximum number of user-defined exceptions I can have in a block?
Oracle does not impose a hard limit on the number of user-defined exceptions. However, each exception name consumes memory in the PL/SQL context area. Practically, you should keep them under 50 per unit. For large numbers of business errors, consider using RAISE_APPLICATION_ERROR with dynamic codes instead.
Was this helpful?
03
How do I log the line number where an exception occurred?
Use DBMS_UTILITY.FORMAT_ERROR_BACKTRACE inside the exception handler. It returns a string with the line numbers of the error stack, e.g., 'ORA-06512: at line 42'.