Mid-level 3 min · March 09, 2026

PL/SQL Exception Handling — Silent Data Loss, NO_DATA_FOUND

Missing FK triggers NO_DATA_FOUND in loop; WHEN OTHERS with ROLLBACK wipes prior insert — batch success, data lost.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • 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
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 Pattern
DECLARE
    v_project_name forge_projects.name%TYPE;
    e_invalid_status EXCEPTION; -- User-defined exception
BEGIN
    -- Standard query that might fail
    SELECT name INTO v_project_name 
    FROM forge_projects 
    WHERE project_id = 999; 

    -- Business logic check
    IF v_project_name IS NULL THEN
        RAISE e_invalid_status;
    END IF;

EXCEPTION
    WHEN 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.');
    WHEN OTHERS THEN
        -- Catch-all for unexpected issues
        DBMS_OUTPUT.PUT_LINE('Unexpected Error Code: ' || SQLCODE || ' - ' || SQLERRM);
        RAISE; -- Re-propagate the error after logging
END;
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.

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 error
DECLARE
    e_fk_violation EXCEPTION;
    PRAGMA EXCEPTION_INIT(e_fk_violation, -2292);  -- ORA-02292: child record found
BEGIN
    DELETE FROM forge_projects WHERE project_id = 100;
EXCEPTION
    WHEN 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');
    WHEN OTHERS THEN
        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 isolation
DECLARE
    v_total NUMBER := 0;
BEGIN
    FOR rec IN (SELECT id FROM forge_tasks WHERE status = 'ACTIVE') LOOP
        BEGIN
            -- Inner block isolates this row's processing
            UPDATE forge_tasks SET processed_date = SYSDATE WHERE id = rec.id;
            v_total := v_total + 1;
        EXCEPTION
            WHEN OTHERS THEN
                DBMS_OUTPUT.PUT_LINE('Skipping task ' || rec.id || ' due to: ' || SQLERRM);
                -- Continue with next record; transaction not rolled back
        END;
    END LOOP;
    DBMS_OUTPUT.PUT_LINE('Processed ' || v_total || ' tasks successfully.');
EXCEPTION
    WHEN OTHERS THEN
        ROLLBACK;
        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.

RaiseComparison.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- io.thecodeforge: RAISE vs RAISE_APPLICATION_ERROR
DECLARE
    e_insufficient_funds EXCEPTION;
    v_balance NUMBER := 50;
    v_withdraw NUMBER := 100;
BEGIN
    IF v_withdraw > v_balance THEN
        -- Option 1: user-defined exception + RAISE
        RAISE e_insufficient_funds;
    END IF;
EXCEPTION
    WHEN e_insufficient_funds THEN
        -- Option 2: re-raise with custom ORA error
        RAISE_APPLICATION_ERROR(-20101, 'Insufficient funds: balance ' || v_balance || ' < ' || v_withdraw);
END;
Output
ORA-20101: Insufficient funds: balance 50 < 100
Choosing the Right Tool
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 blocks
BEGIN
    -- Outer Block
    BEGIN
        -- Inner Block: isolate failures to keep the outer process alive
        INSERT INTO forge_logs (msg) VALUES ('Starting Task');
    EXCEPTION
        WHEN OTHERS THEN
            DBMS_OUTPUT.PUT_LINE('Log failed, but continuing main task.');
    END;

    -- Main business logic continues here
    UPDATE 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.

ErrorLogProcedure.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
26
27
-- io.thecodeforge: Central error logging procedure
CREATE OR REPLACE PACKAGE io_thecodeforge.error_log_pkg AS
    PROCEDURE log_error(
        p_procedure_name VARCHAR2,
        p_sqlcode        NUMBER,
        p_sqlerrm        VARCHAR2,
        p_backtrace      VARCHAR2
    );
END;
/
CREATE OR REPLACE PACKAGE BODY io_thecodeforge.error_log_pkg AS
    PROCEDURE log_error(
        p_procedure_name VARCHAR2,
        p_sqlcode        NUMBER,
        p_sqlerrm        VARCHAR2,
        p_backtrace      VARCHAR2
    ) IS
        PRAGMA AUTONOMOUS_TRANSACTION;
    BEGIN
        INSERT INTO error_log_tbl(
            procedure_name, error_code, error_message, backtrace, logged_at
        ) VALUES (
            p_procedure_name, p_sqlcode, p_sqlerrm, p_backtrace, SYSTIMESTAMP
        );
        COMMIT;
    END;
END;
Output
Package created.
Package body created.
Error Logging Mental Model
  • 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.
● 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
Commands
SET SERVEROUTPUT ON;
DBMS_OUTPUT.PUT_LINE('Error: ' || SQLERRM || ' code: ' || SQLCODE);
Fix now
Wrap the failing statement in a BEGIN-END sub-block and capture the error immediately with WHEN OTHERS THEN log_and_raise;
PRAGMA EXCEPTION_INIT not raising the expected custom error+
Immediate action
Check that the PRAGMA declaration appears before the BEGIN in the declaration section
Commands
SELECT * FROM user_errors WHERE name = 'YOUR_PACKAGE';
SHOW ERRORS;
Fix now
Move the PRAGMA above all variable declarations, and ensure the error number matches exactly (e.g. -2292 for foreign key violation).
RAISE_APPLICATION_ERROR does not roll back transaction+
Immediate action
Add explicit ROLLBACK before RAISE_APPLICATION_ERROR in the exception handler
Commands
ROLLBACK;
RAISE_APPLICATION_ERROR(-20001, 'Business rule violated', TRUE);
Fix now
Always include ROLLBACK before raising user-defined errors in subprograms that modify data.
Exception Types Comparison
Exception TypeDefined ByTypical Use Case
PredefinedOracle EngineCommon errors like NO_DATA_FOUND or TOO_MANY_ROWS
Non-PredefinedDeveloper (using PRAGMA)Standard Oracle ORA-nnnnn codes that lack names
User-DefinedDeveloper (Business Logic)Violations of specific rules (e.g., 'Insufficient Balance')
WHEN OTHERSCatch-all clauseLogging unexpected bugs before re-raising

Key takeaways

1
PL/SQL Exception Handling separates error logic from business logic, keeping code clean and maintainable.
2
There are three exception categories
predefined, non-predefined (PRAGMA), and user-defined — use each for its purpose.
3
Never use WHEN OTHERS THEN NULL
always log and re-raise unless the error is truly recoverable.
4
Exception propagation follows block nesting
inner blocks isolate errors; declaration section errors cannot be caught locally.
5
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'.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
Can I use multiple WHEN clauses for the same exception?
02
What is the maximum number of user-defined exceptions I can have in a block?
03
How do I log the line number where an exception occurred?
🔥

That's PL/SQL. Mark it forged?

3 min read · try the examples if you haven't

Previous
PL/SQL Triggers Explained
7 / 27 · PL/SQL
Next
PL/SQL Packages Explained