WHEN OTHERS without RAISE or FORMAT_ERROR_STACK logging silently swallows every exception — the most common source of invisible production failures in Oracle
Capture FORMAT_ERROR_STACK into a local variable as the FIRST statement in every exception handler — any prior SQL overwrites the error context
RAISE_APPLICATION_ERROR with the third parameter TRUE preserves the original error chain when re-raising — FALSE (the default) discards it permanently
Use PRAGMA AUTONOMOUS_TRANSACTION for error logging so log entries survive transaction rollbacks
Catch at the level where the recovery decision can be made — not at the level where the error occurs
Plain-English First
Exception handling in PL/SQL determines whether your system tells you when something breaks or whether it silently produces wrong answers and you find out three weeks later from an angry customer. The patterns in this guide — how you catch errors, what you log, when you re-raise, how you structure your error codes — are the difference between a system you can debug in five minutes and one that takes a week of forensics to diagnose.
Exception handling in PL/SQL is not about preventing crashes. It is about controlling what happens when errors occur and ensuring every failure is visible, diagnosable, and traceable.
Every exception handler is a design decision with three possible outcomes: propagate the error to the caller unchanged, transform it by adding context and re-raising, or suppress it by catching and continuing. Each outcome is correct in specific circumstances. The damage happens when the wrong outcome is chosen — particularly when errors are suppressed in contexts where they should propagate.
The three most destructive patterns in production PL/SQL systems are: WHEN OTHERS THEN NULL (which converts every failure into silent data corruption), exception handlers that execute SQL before capturing the error stack (which logs wrong or empty error context), and RAISE_APPLICATION_ERROR without the third parameter set to TRUE (which discards the original error from the stack permanently).
This guide covers every production-grade exception handling pattern with the failure mode each prevents and the production consequence of getting it wrong. Every code example is runnable on Oracle 19c, 21c, and 23ai. The patterns are validated across financial transaction processing, healthcare data systems, and logistics platforms processing millions of records daily.
This article is a companion to the ORA-06512 error stack reading guide and the ORA-01403 and ORA-01422 articles in this series. Cross-references are provided where the topics connect.
WHEN OTHERS: The Most Dangerous Exception Handler
WHEN OTHERS catches every exception that is not already caught by a specific handler in the same block. It is the broadest handler in PL/SQL. And when misused, it is the most destructive.
WHEN OTHERS has exactly two correct forms in production code. Form one: capture the full error context and re-raise the original exception with RAISE. This is the standard procedure-level handler — it ensures the error is logged with context and then continues propagating to the caller. Form two: capture the error context, log it, mark the failing record, and continue to the next iteration. This is the batch transaction-level handler — it ensures one failing record does not halt the entire batch.
Every other form of WHEN OTHERS is an error-hiding mechanism. WHEN OTHERS THEN NULL swallows the error entirely — no log entry, no alert, no indication to the caller that anything failed. WHEN OTHERS THEN RETURN returns control to the caller as if the operation succeeded — the caller has no way to distinguish success from silent failure. WHEN OTHERS that returns a default value gives the caller incorrect data from an operation that did not complete.
The correct handler depends on the block level. At the procedure level, log and re-raise. At the batch loop level, log, mark FAILED, and continue. At the API boundary, log the full stack server-side and return a user-safe error response to the client — never expose Oracle error codes, table names, or SQL text.
A practical audit step: grep your entire PL/SQL codebase for WHEN OTHERS THEN NULL and WHEN OTHERS THEN RETURN. Every match is a location where production errors are currently being swallowed. Schedule time to replace every one.
when_others_patterns.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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
-- ============================================================-- PATTERN 1: Log and re-raise (procedure level)-- Standard handler for most procedures-- ============================================================CREATEORREPLACEPROCEDUREcharge_account(
p_account_id INNUMBER,
p_amount INNUMBER
) IS
v_balance NUMBER;
BEGINSELECT balance INTO v_balance
FROM accounts
WHERE account_id = p_account_id;
IF v_balance < p_amount THEN
RAISE_APPLICATION_ERROR(-20001, 'Insufficient balance for account ' || p_account_id);
ENDIF;
UPDATE accounts
SET balance = balance - p_amount
WHERE account_id = p_account_id;
EXCEPTIONWHEN NO_DATA_FOUND THEN-- Specific handler: account does not exist
error_pkg.log_and_raise_custom(
p_module => 'charge_account',
p_message => 'Account not found: ' || p_account_id,
p_errno => -20002
);
WHENOTHERSTHEN-- Generic handler: capture full stack, log, re-raise
error_pkg.log_and_reraise(p_module => 'charge_account');
END charge_account;
/
-- ============================================================-- PATTERN 2: Log and continue (batch transaction level)-- Process each record independently — never halt the batch-- ============================================================CREATEORREPLACEPROCEDUREprocess_payment_batch(
p_batch_date INDATE,
p_processed_count OUTNUMBER,
p_error_count OUTNUMBER
) ISBEGIN
p_processed_count := 0;
p_error_count := 0;
FOR rec IN (
SELECT transaction_id, account_id, amount
FROM pending_transactions
WHERE status = 'PENDING'AND batch_date = p_batch_date
ORDERBY transaction_id
) LOOPBEGIN-- Process individual transactioncharge_account(rec.account_id, rec.amount);
UPDATE pending_transactions
SET status = 'PROCESSED', processed_at = SYSTIMESTAMPWHERE transaction_id = rec.transaction_id;
p_processed_count := p_processed_count + 1;
IFMOD(p_processed_count, 1000) = 0THENCOMMIT;
ENDIF;
EXCEPTIONWHENOTHERSTHEN-- Capture error context FIRST — before any SQLDECLARE
v_stack VARCHAR2(4000) := DBMS_UTILITY.FORMAT_ERROR_STACK;
v_bt VARCHAR2(4000) := DBMS_UTILITY.FORMAT_ERROR_BACKTRACE;
v_code NUMBER := SQLCODE;
BEGIN-- Log the failure using autonomous transactionlog_batch_error(
p_batch_date => p_batch_date,
p_txn_id => rec.transaction_id,
p_error_code => v_code,
p_error_stack => v_stack || CHR(10) || v_bt
);
-- Mark the transaction as failedUPDATE pending_transactions
SET status = 'FAILED',
error_message = SUBSTR(v_stack, 1, 2000)
WHERE transaction_id = rec.transaction_id;
p_error_count := p_error_count + 1;
-- Continue to next transaction — do NOT halt the batchEND;
END;
ENDLOOP;
COMMIT;
-- Post-batch reconciliation: alert if error rate exceeds thresholdIF p_error_count > 0AND (p_processed_count + p_error_count) > 0THENDECLARE
v_error_pct NUMBER := ROUND(
100 * p_error_count / (p_processed_count + p_error_count), 1
);
BEGINIF v_error_pct > 5THEN
RAISE_APPLICATION_ERROR(
-20020,
'Batch completed with ' || v_error_pct || '% error rate ('
|| p_error_count || ' of ' || (p_processed_count + p_error_count)
|| '). Check batch_error_log for details.'
);
ENDIF;
END;
ENDIF;
END process_payment_batch;
/
-- ============================================================-- PATTERN 3: Log and return safe response (API boundary)-- Never expose raw Oracle errors to clients-- ============================================================CREATEORREPLACEFUNCTIONapi_get_balance(
p_account_id INNUMBER
) RETURNVARCHAR2IS
v_balance NUMBER;
BEGINSELECT balance INTO v_balance
FROM accounts
WHERE account_id = p_account_id;
RETURN'{"status":"success","balance":' || v_balance || '}';
EXCEPTIONWHEN NO_DATA_FOUND THEN-- 404 equivalent — account does not existRETURN'{"status":"error","code":"NOT_FOUND","message":"Account not found"}';
WHENOTHERSTHEN-- Log full stack server-side — do not expose to client
error_pkg.log_and_reraise(p_module => 'api_get_balance');
-- If the above re-raises, execution stops here-- The calling web server catches the exception and returns HTTP 500RETURN'{"status":"error","code":"INTERNAL","message":"An unexpected error occurred"}';
END api_get_balance;
/
WHEN OTHERS Anti-Patterns That Hide Production Failures
WHEN OTHERS THEN NULL — swallows every exception silently. No log, no alert, no indication to the caller. The most dangerous single line of PL/SQL in production.
WHEN OTHERS THEN RETURN — returns control to the caller as if the operation succeeded. The caller cannot distinguish success from silent failure.
WHEN OTHERS that returns a default value — gives the caller incorrect data from an operation that crashed midway.
WHEN OTHERS with INSERT before FORMAT_ERROR_STACK — the INSERT overwrites the error buffer. The log captures the INSERT result, not the original error.
WHEN OTHERS without PRAGMA AUTONOMOUS_TRANSACTION in the logging procedure — if the caller rolls back, the error log entry is rolled back too.
Audit rule: grep for WHEN OTHERS in every PL/SQL package. Every instance must either call RAISE or capture FORMAT_ERROR_STACK and log. Every other form is hiding failures.
Production Insight
In a codebase audit across three production Oracle systems, we found an average of 31 WHEN OTHERS THEN NULL instances per schema. Each one was a location where errors had been silently discarded — some for years. The audit and replacement took two days. The alternative — continuing to silently corrupt data — was not an option.
Key Takeaway
WHEN OTHERS has two correct forms: log and re-raise at the procedure level, log and continue at the batch level. Every other form silently hides production failures. Grep your codebase for WHEN OTHERS THEN NULL and remove every instance.
The First-Statement Rule: Capture Error Context Before Any SQL
Oracle maintains exception context — the error code, message, and backtrace — in a session-level buffer. This buffer is overwritten when the next SQL statement executes. Any INSERT, UPDATE, DELETE, COMMIT, SELECT, or even a PL/SQL function call that internally executes SQL will replace the buffer contents.
This means every exception handler must capture FORMAT_ERROR_STACK, FORMAT_ERROR_BACKTRACE, and SQLCODE into local variables as the absolute first operation. No SQL can come first. Not cleanup SQL. Not status update SQL. Not the logging INSERT itself.
The failure mode is subtle: the error log INSERT succeeds, the log entry is created, but the error_stack column contains the result of the INSERT operation (which succeeded) rather than the original error (which triggered the handler). The log entry exists — it just contains the wrong error. During incident response, the team sees an error log entry with an empty or irrelevant stack and concludes the logging is broken when in fact the capture order is wrong.
The correct pattern uses a nested DECLARE block inside the WHEN OTHERS handler. The DECLARE block assigns all three error capture functions to local variables immediately — this is pure PL/SQL assignment, not SQL, so it does not overwrite the buffer. After assignment, any SQL can be executed safely because the original error context is preserved in the local variables.
For reusable error logging, extract the capture-and-log sequence into a separate procedure. The procedure must capture FORMAT_ERROR_STACK into its own local variables at the very top of its body — before the INSERT — because calling the procedure itself does not overwrite the buffer, but the INSERT inside it will.
error_capture_order.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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
-- ============================================================-- WRONG: SQL before error capture — buffer is overwritten-- ============================================================CREATEORREPLACEPROCEDURE wrong_capture_order IS
v_dummy VARCHAR2(1);
BEGINSELECT dummy INTO v_dummy FROM dual WHERE1 = 0; -- Raises ORA-01403EXCEPTIONWHENOTHERSTHEN-- This INSERT overwrites the error bufferINSERTINTOapp_error_log (module, error_stack, created_at)
VALUES ('wrong_capture', DBMS_UTILITY.FORMAT_ERROR_STACK, SYSTIMESTAMP);
-- ^^^ FORMAT_ERROR_STACK is called AFTER the INSERT-- The INSERT succeeded — the buffer now contains the INSERT result-- The log entry shows empty or wrong error informationCOMMIT;
RAISE;
END wrong_capture_order;
/
-- ============================================================-- CORRECT: Capture into local variables FIRST, then SQL-- ============================================================CREATEORREPLACEPROCEDURE correct_capture_order IS
v_dummy VARCHAR2(1);
BEGINSELECT dummy INTO v_dummy FROM dual WHERE1 = 0;
EXCEPTIONWHENOTHERSTHEN-- Step 1: Capture ALL error context as first statements-- These are PL/SQL assignments, not SQL — they do not overwrite the bufferDECLARE
v_error_stack VARCHAR2(4000) := DBMS_UTILITY.FORMAT_ERROR_STACK;
v_backtrace VARCHAR2(4000) := DBMS_UTILITY.FORMAT_ERROR_BACKTRACE;
v_error_code NUMBER := SQLCODE;
v_call_stack VARCHAR2(4000) := DBMS_UTILITY.FORMAT_CALL_STACK;
BEGIN-- Step 2: Now safe to execute SQL — error context is in local variablesINSERTINTOapp_error_log (
module, error_code, error_stack, backtrace, call_stack, created_at
) VALUES (
'correct_capture', v_error_code, v_error_stack,
v_backtrace, v_call_stack, SYSTIMESTAMP
);
COMMIT;
-- Step 3: Re-raise the original exceptionRAISE;
END;
END correct_capture_order;
/
-- ============================================================-- CORRECT: Reusable autonomous transaction logging procedure-- Captures error context at the top, logs with independent commit-- ============================================================CREATEORREPLACEPROCEDURElog_error(
p_module INVARCHAR2,
p_context_info INVARCHAR2DEFAULTNULL
) IS-- Capture error context IMMEDIATELY — before any SQL in this procedure
v_error_stack VARCHAR2(4000) := DBMS_UTILITY.FORMAT_ERROR_STACK;
v_backtrace VARCHAR2(4000) := DBMS_UTILITY.FORMAT_ERROR_BACKTRACE;
v_error_code NUMBER := SQLCODE;
v_call_stack VARCHAR2(4000) := DBMS_UTILITY.FORMAT_CALL_STACK;
-- Autonomous transaction: commits independently of callerPRAGMA AUTONOMOUS_TRANSACTION;
BEGININSERTINTOapp_error_log (
module, error_code, error_stack, backtrace, call_stack,
context_info, created_at
) VALUES (
p_module, v_error_code, v_error_stack, v_backtrace, v_call_stack,
p_context_info, SYSTIMESTAMP
);
COMMIT; -- Commits only this autonomous transaction-- Caller's transaction is unaffected — rollback does not erase this log entryEND log_error;
/
-- ============================================================-- Usage: clean exception handler with reusable logging-- ============================================================CREATEORREPLACEPROCEDUREprocess_refund(
p_order_id INNUMBER,
p_amount INNUMBER
) IS
v_status VARCHAR2(20);
BEGINSELECT status INTO v_status
FROM orders
WHERE order_id = p_order_id;
IF v_status != 'COMPLETED'THEN
RAISE_APPLICATION_ERROR(-20010,
'Cannot refund order ' || p_order_id || ' — status is ' || v_status);
ENDIF;
UPDATE orders SET status = 'REFUNDED'WHERE order_id = p_order_id;
UPDATE accounts SET balance = balance + p_amount WHERE order_id = p_order_id;
EXCEPTIONWHEN NO_DATA_FOUND THENlog_error('process_refund', 'order_id=' || p_order_id);
RAISE_APPLICATION_ERROR(-20002,
'Order not found: ' || p_order_id, TRUE);
WHENOTHERSTHENlog_error('process_refund', 'order_id=' || p_order_id || ', amount=' || p_amount);
RAISE; -- Re-raise original exceptionEND process_refund;
/
The Error Buffer Model
The buffer is session-level — any SQL in any block overwrites it
INSERT, UPDATE, DELETE, COMMIT, and SELECT all overwrite the buffer on success or failure
FORMAT_ERROR_STACK, FORMAT_ERROR_BACKTRACE, and SQLCODE all read from the same buffer
PL/SQL variable assignment (:= DBMS_UTILITY.FORMAT_ERROR_STACK) reads the buffer without overwriting it — this is why capture into local variables works
After capture, the buffer can be safely overwritten — the original error is preserved in the local variables
PRAGMA AUTONOMOUS_TRANSACTION in the logging procedure ensures the log INSERT commits independently of the caller
Production Insight
Error logs that show empty stacks or wrong errors are almost always caused by SQL executing before FORMAT_ERROR_STACK capture. The logging INSERT succeeded — the log entry exists — but it contains the INSERT's result context, not the original error. During incident response, the team sees log entries and concludes the errors were minor or non-diagnostic. The real error was never captured.
Key Takeaway
Capture FORMAT_ERROR_STACK, FORMAT_ERROR_BACKTRACE, and SQLCODE into local variables as the absolute first operation in every exception handler. Use PRAGMA AUTONOMOUS_TRANSACTION in the logging procedure so log entries survive transaction rollbacks. The first-statement rule is not a guideline — it is the only way to capture correct error context.
RAISE_APPLICATION_ERROR: Custom Errors with Stack Preservation
RAISE_APPLICATION_ERROR raises a user-defined exception with a custom error code between -20000 and -20999 and a message of up to 2048 bytes. It is the primary mechanism for raising business-meaningful errors from PL/SQL to calling code.
The parameter that most developers miss is the third one — called keep_errors in Oracle documentation. When TRUE, the original error chain is preserved in the stack. The custom error is appended on top of whatever errors already exist. When FALSE — which is the default — the existing error stack is replaced entirely. Only the custom error and its own ORA-06512 entries remain.
The consequence of using FALSE when re-raising a caught exception: the root cause error (the ORA-XXXXX that triggered the exception handler) is permanently discarded from the stack. The error log shows only the custom message. Post-mortem debugging cannot determine why the error occurred — only that it was caught and re-raised.
The correct pattern: capture FORMAT_ERROR_STACK first (following the first-statement rule), then call RAISE_APPLICATION_ERROR with TRUE as the third parameter and include the original error text in the custom message. This preserves both the business context (the custom message) and the root cause (the original error chain).
For new errors that are not re-raises of caught exceptions — validation failures, business rule violations, authorization checks — use FALSE (the default) because there is no prior error to preserve.
A consistent error code scheme across the application enables programmatic handling by callers, log filtering by error type, and automated alerting based on error category.
raise_application_error_patterns.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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
-- ============================================================-- WRONG: keep_errors defaults to FALSE — original error is lost-- ============================================================DECLARE
v_result NUMBER;
BEGIN
v_result := 1 / 0; -- Raises ORA-01476: divisor is equal to zeroEXCEPTIONWHENOTHERSTHEN-- FALSE is the default — discards ORA-01476 from the stack
RAISE_APPLICATION_ERROR(-20000, 'Calculation failed');
-- Error stack now shows ONLY: ORA-20000: Calculation failed-- ORA-01476 is gone — post-mortem cannot determine root causeEND;
/
-- ============================================================-- CORRECT: keep_errors TRUE — original error chain preserved-- ============================================================DECLARE
v_result NUMBER;
BEGIN
v_result := 1 / 0;
EXCEPTIONWHENOTHERSTHENDECLARE
v_original VARCHAR2(4000) := DBMS_UTILITY.FORMAT_ERROR_STACK;
BEGIN-- TRUE preserves the original error in the stack
RAISE_APPLICATION_ERROR(
-20010,
'Tax calculation failed for order processing | ' || v_original,
TRUE-- keep_errors: preserve original error chain
);
END;
-- Error stack now shows:-- ORA-20010: Tax calculation failed for order processing | ORA-01476: divisor is equal to zero-- ORA-06512: at line 12-- ORA-01476: divisor is equal to zero <- Original error preservedEND;
/
-- ============================================================-- CORRECT: New error (not a re-raise) — FALSE is appropriate-- ============================================================CREATEORREPLACEPROCEDUREvalidate_order_amount(
p_amount INNUMBER
) ISBEGINIF p_amount ISNULLTHEN
RAISE_APPLICATION_ERROR(-20001, 'Order amount cannot be null');
-- FALSE (default) is correct here — no prior error to preserveENDIF;
IF p_amount <= 0THEN
RAISE_APPLICATION_ERROR(-20001, 'Order amount must be positive: ' || p_amount);
ENDIF;
IF p_amount > 1000000THEN
RAISE_APPLICATION_ERROR(-20001, 'Order amount exceeds maximum: ' || p_amount);
ENDIF;
END validate_order_amount;
/
-- ============================================================-- Centralized error code constants-- One package, all codes documented, enforced by code review-- ============================================================CREATEORREPLACEPACKAGE app_errors AS-- Validation errors: -20001 to -20009
c_validation CONSTANTNUMBER := -20001;
-- Not found errors: -20010 to -20019
c_not_found CONSTANTNUMBER := -20010;
-- Authorization errors: -20020 to -20029
c_auth_denied CONSTANTNUMBER := -20020;
-- Business rule violations: -20030 to -20039
c_business_rule CONSTANTNUMBER := -20030;
-- External service errors: -20040 to -20049
c_external_svc CONSTANTNUMBER := -20040;
-- System/internal errors: -20100 to -20199
c_system_error CONSTANTNUMBER := -20100;
-- Raise validation with field contextPROCEDUREraise_validation(p_field INVARCHAR2, p_message INVARCHAR2);
-- Raise not-found with entity contextPROCEDUREraise_not_found(p_entity INVARCHAR2, p_id INVARCHAR2);
-- Re-raise with business context — preserves original stackPROCEDUREreraise_with_context(p_module INVARCHAR2, p_context INVARCHAR2);
-- Translate error code to user-safe messageFUNCTIONuser_message(p_error_code INNUMBER) RETURNVARCHAR2;
END app_errors;
/
CREATEORREPLACEPACKAGEBODY app_errors ASPROCEDUREraise_validation(p_field INVARCHAR2, p_message INVARCHAR2) ISBEGIN
RAISE_APPLICATION_ERROR(c_validation,
'Validation [' || p_field || ']: ' || p_message);
END;
PROCEDUREraise_not_found(p_entity INVARCHAR2, p_id INVARCHAR2) ISBEGIN
RAISE_APPLICATION_ERROR(c_not_found,
p_entity || ' not found: ' || p_id);
END;
PROCEDUREreraise_with_context(p_module INVARCHAR2, p_context INVARCHAR2) IS
v_original VARCHAR2(4000) := DBMS_UTILITY.FORMAT_ERROR_STACK;
BEGINlog_error(p_module, p_context); -- Autonomous transaction logging
RAISE_APPLICATION_ERROR(
c_system_error,
p_context || ' | ' || SUBSTR(v_original, 1, 800),
TRUE-- Preserve original error chain
);
END;
FUNCTIONuser_message(p_error_code INNUMBER) RETURNVARCHAR2ISBEGINRETURNCASEWHEN p_error_code BETWEEN c_validation AND c_validation + 8THEN'Invalid input provided'WHEN p_error_code BETWEEN c_not_found AND c_not_found + 9THEN'Requested item not found'WHEN p_error_code BETWEEN c_auth_denied AND c_auth_denied + 9THEN'Access denied'WHEN p_error_code BETWEEN c_business_rule AND c_business_rule + 9THEN'Operation not allowed'WHEN p_error_code BETWEEN c_external_svc AND c_external_svc + 9THEN'External service unavailable — please retry'ELSE'An unexpected error occurred'END;
END;
END app_errors;
/
keep_errors Parameter: When to Use TRUE vs FALSE
TRUE: use when re-raising a caught exception with additional business context. The caller sees both your custom message and the original Oracle error. Post-mortem analysis can trace the full chain. FALSE (default): use when raising a new error that is not related to a previously caught exception — validation failures, business rule checks, authorization denials. There is no prior error to preserve. The rule: if your RAISE_APPLICATION_ERROR is inside a WHEN OTHERS or WHEN specific_exception handler, use TRUE. If it is in regular procedural code, FALSE is correct.
Production Insight
I have seen three separate production systems where every RAISE_APPLICATION_ERROR used the default FALSE inside WHEN OTHERS handlers. In each case, the error logs contained thousands of entries with custom messages but zero root cause information. The teams could see that errors occurred but could not determine why. Fixing the third parameter to TRUE immediately transformed the error logs from useless to diagnostic.
Key Takeaway
RAISE_APPLICATION_ERROR with keep_errors TRUE preserves the original error chain. Without it, the root cause is permanently discarded. Use TRUE inside exception handlers, FALSE in procedural code. A consistent error code scheme enables programmatic handling and log filtering.
Exception Propagation: Catch at the Right Level
Not every exception should be caught. The decision depends on the abstraction level and whether the current block can make a meaningful recovery decision.
The propagation principle: catch at the level where the recovery decision can be made, not at the level where the error occurs. Inner functions that perform data lookups should let NO_DATA_FOUND propagate to the calling procedure, which has the business context to decide whether missing data is an error or a default case. The lookup function does not have that context.
The anti-pattern is catching too early. An inner block catches NO_DATA_FOUND and raises a generic ORA-20000 error. The outer block — which would have known exactly how to handle NO_DATA_FOUND — now sees only a generic custom error. It cannot distinguish a missing account from a missing configuration from a missing user preference. The specific exception type, which the outer block needed to make a recovery decision, was destroyed by the inner block's premature catch.
PL/SQL exception propagation follows a clear path: if the current block has no handler for the exception, it propagates to the enclosing block. If the enclosing block has no handler, it propagates further outward. If no handler exists at any level, the exception reaches the calling client.
The correct structure has four layers. Inner functions: no handlers — let specific exceptions propagate naturally. Procedure level: catch specific exceptions where you can add context, catch WHEN OTHERS to log and re-raise. Batch level: catch WHEN OTHERS to log, mark the record FAILED, and continue. API boundary: catch WHEN OTHERS to log the full stack server-side and return a safe response to the client.
propagation_patterns.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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
-- ============================================================-- Inner function: NO exception handler-- Let the caller decide what missing data means-- ============================================================CREATEORREPLACEFUNCTIONget_balance(
p_account_id INNUMBER
) RETURNNUMBERIS
v_balance NUMBER;
BEGINSELECT balance INTO v_balance
FROM accounts
WHERE account_id = p_account_id;
RETURN v_balance;
-- NO exception handler — NO_DATA_FOUND propagates to caller-- The caller has business context to decide the responseEND get_balance;
/
-- ============================================================-- Procedure level: catch with context, re-raise-- Has business context to make recovery decisions-- ============================================================CREATEORREPLACEPROCEDUREprocess_payment(
p_account_id INNUMBER,
p_amount INNUMBER
) IS
v_balance NUMBER;
BEGIN
v_balance := get_balance(p_account_id);
-- If get_balance raises NO_DATA_FOUND, we catch it below-- with full business contextIF v_balance < p_amount THEN
app_errors.raise_validation('amount',
'Insufficient balance (' || v_balance || ') for payment of ' || p_amount);
ENDIF;
UPDATE accounts SET balance = balance - p_amount
WHERE account_id = p_account_id;
EXCEPTIONWHEN NO_DATA_FOUND THEN-- We know the context: account_id was not found in accounts tablelog_error('process_payment', 'account_id=' || p_account_id);
app_errors.raise_not_found('Account', TO_CHAR(p_account_id));
WHENOTHERSTHENlog_error('process_payment',
'account_id=' || p_account_id || ', amount=' || p_amount);
RAISE;
END process_payment;
/
-- ============================================================-- Batch level: catch, log, mark failed, continue-- ============================================================CREATEORREPLACEPROCEDURE run_daily_payments IS
v_ok NUMBER := 0;
v_err NUMBER := 0;
BEGINFOR rec IN (SELECT * FROM payment_queue WHERE status = 'PENDING') LOOPBEGINprocess_payment(rec.account_id, rec.amount);
UPDATE payment_queue SET status = 'DONE'WHERE queue_id = rec.queue_id;
v_ok := v_ok + 1;
EXCEPTIONWHENOTHERSTHENDECLARE
v_stack VARCHAR2(4000) := DBMS_UTILITY.FORMAT_ERROR_STACK;
BEGINlog_error('run_daily_payments',
'queue_id=' || rec.queue_id || ', account=' || rec.account_id);
UPDATE payment_queue
SET status = 'FAILED', error_msg = SUBSTR(v_stack, 1, 2000)
WHERE queue_id = rec.queue_id;
v_err := v_err + 1;
END;
END;
ENDLOOP;
COMMIT;
DBMS_OUTPUT.PUT_LINE('Completed: ' || v_ok || ' ok, ' || v_err || ' failed');
END run_daily_payments;
/
-- ============================================================-- API boundary: catch, log server-side, return safe response-- ============================================================CREATEORREPLACEFUNCTIONapi_process_payment(
p_account_id INNUMBER,
p_amount INNUMBER
) RETURNVARCHAR2ISBEGINprocess_payment(p_account_id, p_amount);
RETURN'{"status":"success"}';
EXCEPTIONWHENOTHERSTHENDECLARE
v_code NUMBER := SQLCODE;
BEGINlog_error('api_process_payment',
'account_id=' || p_account_id || ', amount=' || p_amount);
-- Map Oracle error code to user-safe messageRETURN'{"status":"error","message":"'
|| app_errors.user_message(v_code) || '"}';
-- Never expose ORA-XXXXX codes, table names, or SQL text to clientsEND;
END api_process_payment;
/
Four-Layer Exception Architecture
Inner functions: no handlers — let exceptions propagate. The function does not have business context to decide the response.
Procedure level: catch specific exceptions where you have context. Add logging. Re-raise with business information.
Batch level: catch WHEN OTHERS to log, mark FAILED, and continue. One failing record must not halt 500,000 others.
API boundary: catch WHEN OTHERS to log the full stack server-side and return a safe, generic message to the client.
Catching at any other layer — particularly catching too early in inner functions — destroys exception type information that outer layers need.
Production Insight
The most common propagation mistake is developers adding WHEN OTHERS handlers to inner utility functions 'for safety.' Every such handler is a context-destroying checkpoint that prevents the calling procedure from making correct recovery decisions. Utility functions should have zero exception handlers unless they can genuinely recover from the error without caller involvement.
Key Takeaway
Catch at the level where the recovery decision can be made. Inner functions: no handlers. Procedure level: log and re-raise with context. Batch level: log, mark failed, continue. API boundary: log server-side, return safe message. Catching too early destroys the exception type information that outer layers need.
Testing Exception Handlers: Forcing Every Error Path
Exception handlers that have never been triggered in a test environment will fail when they fire in production. The failure mode is not a crash — it is a handler that appears to work but produces incorrect output: logs to the wrong table, returns the wrong default, raises a different exception than intended, or has a typo in the error message that omits the diagnostic key value.
The testing strategy has three components. First, force each specific exception by creating the conditions that trigger it: pass a non-existent ID for NO_DATA_FOUND, insert a duplicate for TOO_MANY_ROWS, call with an unauthorized role for authorization checks. Second, verify the handler's output: correct return value, correct error log entry with non-empty stack, correct error code in the raised exception. Third, verify the batch skip-and-continue behavior: insert a mix of valid and invalid records, run the batch, confirm all valid records processed and all invalid records marked FAILED with error context.
Automate all exception tests. Manual testing of error paths is unreliable because developers naturally focus on the happy path. An automated test that forces NO_DATA_FOUND on every deploy catches handler regressions before they reach production.
test_exception_handlers.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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
-- ============================================================-- Test 1: NO_DATA_FOUND handler raises correct custom error-- ============================================================CREATEORREPLACEPROCEDURE test_not_found_raises ISBEGINBEGINprocess_payment(-99999, 100); -- Non-existent account-- If we reach this line, the handler did not raise
RAISE_APPLICATION_ERROR(-20901, 'FAIL: expected exception but got success');
EXCEPTIONWHENOTHERSTHENIFSQLCODE = app_errors.c_not_found THEN
DBMS_OUTPUT.PUT_LINE('PASS: NO_DATA_FOUND raises c_not_found (-20010)');
ELSIFSQLCODE = -20901THEN
DBMS_OUTPUT.PUT_LINE('FAIL: handler did not raise — returned silently');
RAISE;
ELSE
DBMS_OUTPUT.PUT_LINE('FAIL: expected -20010, got ' || SQLCODE);
RAISE;
ENDIF;
END;
END test_not_found_raises;
/
-- ============================================================-- Test 2: WHEN OTHERS logs correctly and re-raises-- ============================================================CREATEORREPLACEPROCEDURE test_when_others_logs IS
v_log_before NUMBER;
v_log_after NUMBER;
BEGINSELECTCOUNT(*) INTO v_log_before
FROM app_error_log WHERE module = 'process_refund';
BEGIN-- Force an error: refund for non-existent orderprocess_refund(-99999, 50);
RAISE_APPLICATION_ERROR(-20902, 'FAIL: expected exception');
EXCEPTIONWHENOTHERSTHENIFSQLCODENOTIN (-20002, -20902) THEN
DBMS_OUTPUT.PUT_LINE('FAIL: unexpected error code ' || SQLCODE);
RAISE;
ENDIF;
IFSQLCODE = -20902THEN
DBMS_OUTPUT.PUT_LINE('FAIL: handler did not raise');
RAISE;
ENDIF;
END;
SELECTCOUNT(*) INTO v_log_after
FROM app_error_log WHERE module = 'process_refund';
IF v_log_after > v_log_before THEN-- Verify the log entry has a non-empty stackDECLARE
v_stack_len NUMBER;
BEGINSELECTNVL(LENGTH(error_stack), 0) INTO v_stack_len
FROM app_error_log
WHERE module = 'process_refund'ORDERBY created_at DESCFETCHFIRST1ROWSONLY;
IF v_stack_len > 10THEN
DBMS_OUTPUT.PUT_LINE('PASS: WHEN OTHERS logged with non-empty stack');
ELSE
DBMS_OUTPUT.PUT_LINE('FAIL: log entry has empty or short stack (' || v_stack_len || ' chars)');
ENDIF;
END;
ELSE
DBMS_OUTPUT.PUT_LINE('FAIL: no log entry created');
ENDIF;
END test_when_others_logs;
/
-- ============================================================-- Test 3: Batch skip-and-continue-- ============================================================CREATEORREPLACEPROCEDURE test_batch_skip IS
v_ok NUMBER;
v_err NUMBER;
BEGIN-- Setup: 5 queue items, item 3 has non-existent accountDELETEFROM payment_queue WHERE queue_id BETWEEN90001AND90005;
INSERTINTOpayment_queue (queue_id, account_id, amount, status)
SELECT90000 + level,
CASEWHEN level = 3THEN -1ELSE level END,
100, 'PENDING'FROM dual CONNECTBY level <= 5;
COMMIT;
-- Ensure test accounts exist (except -1)FOR i IN1..5LOOPIF i != 3THENBEGININSERTINTOaccounts (account_id, balance, status)
VALUES (i, 10000, 'ACTIVE');
EXCEPTIONWHEN DUP_VAL_ON_INDEX THENNULL; END;
ENDIF;
ENDLOOP;
COMMIT;
-- Run batch
run_daily_payments;
-- Verify resultsSELECTCOUNT(CASEWHEN status = 'DONE'THEN1END),
COUNT(CASEWHEN status = 'FAILED'THEN1END)
INTO v_ok, v_err
FROM payment_queue
WHERE queue_id BETWEEN90001AND90005;
IF v_ok = 4AND v_err = 1THEN
DBMS_OUTPUT.PUT_LINE('PASS: batch processed 4, skipped 1 failure');
ELSE
DBMS_OUTPUT.PUT_LINE('FAIL: expected 4 ok + 1 err, got ' || v_ok || ' ok + ' || v_err || ' err');
ENDIF;
-- CleanupDELETEFROM payment_queue WHERE queue_id BETWEEN90001AND90005;
COMMIT;
EXCEPTIONWHENOTHERSTHENDELETEFROM payment_queue WHERE queue_id BETWEEN90001AND90005;
COMMIT;
RAISE;
END test_batch_skip;
/
-- ============================================================-- Run all tests-- ============================================================BEGIN
DBMS_OUTPUT.PUT_LINE('=== Exception Handler Tests ===');
DBMS_OUTPUT.PUT_LINE('');
test_not_found_raises;
test_when_others_logs;
test_batch_skip;
DBMS_OUTPUT.PUT_LINE('');
DBMS_OUTPUT.PUT_LINE('=== All tests complete ===');
END;
/
Exception Handler Testing Checklist
Force every NO_DATA_FOUND handler by passing a non-existent key — verify the correct custom error code is raised
Force WHEN OTHERS by triggering an unexpected error — verify the handler logs a non-empty error stack and re-raises
Force batch skip-and-continue by inserting a failing record — verify valid records process and the failure is logged with context
Verify the error log entry has: non-empty error_stack, correct module name, correct error_code, and a created_at timestamp
Automate all exception tests — manual testing of error paths is unreliable and will not catch regressions
Production Insight
The holiday weekend incident was caused by a WHEN OTHERS THEN NULL handler that had existed in the codebase for four years. It had never been tested because the test suite covered only the happy path. A single automated test that forced an exception on the payment processing procedure would have caught it before the first deployment.
Key Takeaway
Untested exception handlers are the most dangerous code in production. Force every handler in automated tests: verify the correct error code is raised, the log entry has a non-empty stack, and batch processing continues past failures. If a handler has never been triggered in a test, it has never been validated.
● Production incidentPOST-MORTEMseverity: high
WHEN OTHERS THEN NULL Silently Swallowed 14,000 Failed Transactions Over a Holiday Weekend
Symptom
Monday morning: 2,300 customer complaints about missing payment confirmations. The payment processing dashboard showed 100% success for the weekend batch. No errors appeared in the application error log. The database showed 14,000 fewer completed transactions than expected for a typical weekend volume. The batch scheduler showed the job completed on time with exit code 0.
Assumption
The team assumed a network partition had prevented the batch from running. They spent four hours checking network logs, message queue health, and scheduler configuration before a junior engineer checked the raw database transaction counts and discovered the batch had run and completed — it had just processed zero payments successfully while reporting success.
Root cause
Every procedure in the payment processing package had a WHEN OTHERS THEN NULL exception handler at its outermost block. This pattern had been copied from a template created years earlier by a developer who intended it as a temporary placeholder during development. It was never replaced with proper error handling.
During the holiday weekend, a third-party payment gateway experienced intermittent HTTP 503 errors. For each of the 14,000 affected transactions, the gateway call raised an exception, WHEN OTHERS caught it, NULL executed, and the procedure returned control to the batch loop. The batch loop interpreted the return as success. No error was logged. No status was updated. The transaction remained in PENDING status indefinitely.
The dashboard showed 100% completion because it counted completed batch runs, not successful transactions. The metric was measuring the wrong thing.
Fix
Removed every WHEN OTHERS THEN NULL from the codebase — 23 instances across 8 packages. Replaced each with a pattern appropriate to its block level: WHEN OTHERS that captures FORMAT_ERROR_STACK and calls RAISE at the procedure level; WHEN OTHERS that logs the error, marks the transaction FAILED, and continues at the batch loop level. Added a post-batch reconciliation procedure that compares expected transaction count against actual completed count and raises an alert if the counts diverge by more than 1%. Changed the dashboard to report successful transactions, not successful batch runs.
Key lesson
WHEN OTHERS THEN NULL converts every failure into silent success — the most dangerous single line of PL/SQL in production
Dashboard metrics that measure batch completion instead of transaction success create a false confidence that masks systemic failures
Post-batch reconciliation — comparing expected vs. actual counts — catches silent failures that no individual error handler can detect
Grep your entire PL/SQL codebase for WHEN OTHERS THEN NULL today. Every instance is a time bomb. Remove all of them.
Production debug guideDiagnosing why errors are hidden, misreported, or lost in PL/SQL systems7 entries
Symptom · 01
Error occurs in the application but no entry appears in the error log table
→
Fix
A WHEN OTHERS handler is swallowing the exception without logging. Search for WHEN OTHERS in the call chain: SELECT line, text FROM all_source WHERE name = 'PKG_NAME' AND type = 'PACKAGE BODY' AND UPPER(text) LIKE '%WHEN OTHERS%'. Verify every match either calls RAISE or logs FORMAT_ERROR_STACK before continuing.
Symptom · 02
Error log entry exists but shows an empty error stack or wrong error code
→
Fix
SQL was executed before FORMAT_ERROR_STACK was captured. Oracle overwrites the error buffer on the next SQL statement. Fix: capture FORMAT_ERROR_STACK, FORMAT_ERROR_BACKTRACE, and SQLCODE into local variables as the FIRST statements in the handler — before any INSERT, UPDATE, COMMIT, or SELECT.
Symptom · 03
Error log shows a custom error code (ORA-20xxx) but no original Oracle error
→
Fix
RAISE_APPLICATION_ERROR was called with the third parameter (keep_errors) set to FALSE or omitted (FALSE is the default). The original error chain was discarded. Fix: use RAISE_APPLICATION_ERROR(errno, message, TRUE) when re-raising a caught exception to preserve the original stack.
Symptom · 04
Every error in the log shows the same error code (-20000) regardless of failure type
→
Fix
The application is using a single error code for all custom errors. Implement a consistent error code scheme with ranges by module and error type. Use a centralized error package that defines all error codes as named constants.
Symptom · 05
Error log entry disappears after a transaction rollback
→
Fix
The error logging procedure is not using PRAGMA AUTONOMOUS_TRANSACTION. The INSERT into the error log table is part of the same transaction that failed. When the caller rolls back, the error log INSERT is also rolled back. Fix: add PRAGMA AUTONOMOUS_TRANSACTION to the logging procedure so its COMMIT is independent of the caller's transaction.
Symptom · 06
Raw Oracle error messages are reaching the client application or end user
→
Fix
No API-boundary exception handler exists, or it is re-raising the raw exception. Add a top-level WHEN OTHERS handler at the API boundary that logs the full Oracle stack server-side and returns a user-safe error message. Never expose ORA-XXXXX codes, table names, or SQL text to external clients.
Symptom · 07
Batch job reports 100% success but transaction counts do not match
→
Fix
The batch loop has a WHEN OTHERS handler that catches and swallows exceptions without updating the transaction status. The batch loop sees the return from the handler as success. Fix: every batch-level WHEN OTHERS must mark the failing record as FAILED and increment an error counter. Add post-batch reconciliation.
★ PL/SQL Exception Handling Quick Debug Cheat SheetFast diagnostics for exception handling problems. Run against the database where the error occurred.
Need to find all WHEN OTHERS handlers in a package−
Immediate action
Search ALL_SOURCE for WHEN OTHERS and classify each handler
Commands
SELECT line, TRIM(text) AS code FROM all_source WHERE name = 'PKG_NAME' AND type = 'PACKAGE BODY' AND UPPER(text) LIKE '%WHEN OTHERS%' ORDER BY line;
SELECT line, TRIM(text) AS code FROM all_source WHERE name = 'PKG_NAME' AND type = 'PACKAGE BODY' AND UPPER(text) LIKE '%THEN NULL%' ORDER BY line;
Fix now
Every WHEN OTHERS must either call RAISE or capture FORMAT_ERROR_STACK and log before continuing. Every THEN NULL match is a silent failure point. Remove all of them.
Need to verify error logging procedure uses autonomous transactions+
Immediate action
Check the logging procedure for PRAGMA AUTONOMOUS_TRANSACTION
Commands
SELECT line, TRIM(text) AS code FROM all_source WHERE name = 'ERROR_LOG_PKG' AND type = 'PACKAGE BODY' AND UPPER(text) LIKE '%AUTONOMOUS%' ORDER BY line;
SELECT line, TRIM(text) AS code FROM all_source WHERE name = 'ERROR_LOG_PKG' AND type = 'PACKAGE BODY' AND UPPER(text) LIKE '%COMMIT%' ORDER BY line;
Fix now
If no AUTONOMOUS_TRANSACTION is found, error log entries are lost on rollback. Add PRAGMA AUTONOMOUS_TRANSACTION to the logging procedure and add COMMIT inside it.
Need to check if FORMAT_ERROR_STACK is captured before SQL in exception handlers+
Immediate action
Look for INSERT or UPDATE statements that appear before FORMAT_ERROR_STACK in exception blocks
Commands
SELECT line, TRIM(text) AS code FROM all_source WHERE name = 'PKG_NAME' AND type = 'PACKAGE BODY' AND (UPPER(text) LIKE '%FORMAT_ERROR%' OR UPPER(text) LIKE '%INSERT INTO%error%' OR UPPER(text) LIKE '%WHEN OTHERS%') ORDER BY line;
SELECT line, TRIM(text) AS code FROM all_source WHERE name = 'PKG_NAME' AND type = 'PACKAGE BODY' AND UPPER(text) LIKE '%SQLCODE%' ORDER BY line;
Fix now
Within each exception block, FORMAT_ERROR_STACK assignments must appear before any INSERT, UPDATE, or COMMIT. If they appear after, the captured stack is wrong or empty.
Need to check recent error log entries and their completeness+
Immediate action
Query the error log and check for empty stacks
Commands
SELECT log_id, module, error_code, CASE WHEN error_stack IS NULL OR LENGTH(error_stack) < 10 THEN 'EMPTY/SHORT' ELSE 'PRESENT' END AS stack_status, created_at FROM app_error_log WHERE created_at > SYSTIMESTAMP - INTERVAL '24' HOUR ORDER BY created_at DESC FETCH FIRST 50 ROWS ONLY;
SELECT module, COUNT(*) AS empty_stack_count FROM app_error_log WHERE (error_stack IS NULL OR LENGTH(error_stack) < 10) AND created_at > SYSTIMESTAMP - INTERVAL '7' DAY GROUP BY module ORDER BY COUNT(*) DESC;
Fix now
Modules with empty stack counts have handlers that execute SQL before FORMAT_ERROR_STACK. Fix the capture order in those modules.
Exception Handling Patterns Comparison
Pattern
Catches
Preserves Original Stack
Best Use
Risk if Misused
WHEN NO_DATA_FOUND
ORA-01403 only
Yes (propagates unchanged if re-raised with RAISE)
Specific handling for missing data — return default or raise business error
Returning a default when missing data is a configuration bug hides the problem
WHEN TOO_MANY_ROWS
ORA-01422 only
Yes (propagates unchanged if re-raised with RAISE)
Specific handling for duplicate data — always raise a diagnostic error
Returning a default when duplicates exist hides a data model bug
WHEN OTHERS + RAISE
All unhandled exceptions
Yes — RAISE propagates the original exception unchanged
Procedure-level default handler: log the error, then let it propagate
None — this is the safest generic handler
WHEN OTHERS + continue
All exceptions in a loop iteration
Yes — captured via FORMAT_ERROR_STACK into local variables before SQL
Batch transaction-level: log failure, mark record FAILED, process next
Must capture FORMAT_ERROR_STACK first or the log captures wrong context
RAISE_APPLICATION_ERROR with TRUE
N/A — raises a new exception
Yes — TRUE preserves the original error chain in the stack
Re-raising a caught exception with additional business context
None — this is the correct pattern for context-enriched re-raises
RAISE_APPLICATION_ERROR with FALSE
N/A — raises a new exception
No — FALSE replaces the entire stack
Raising a new error unrelated to a prior exception (validation, auth)
Using FALSE inside an exception handler permanently discards the root cause
WHEN OTHERS THEN NULL
All exceptions
No — exception is silently discarded
Never appropriate in production code
Converts every failure into silent data corruption
Key takeaways
1
WHEN OTHERS has exactly two correct forms
log and re-raise (procedure level), or log and continue (batch level). Every other form silently hides production failures.
2
Capture FORMAT_ERROR_STACK, FORMAT_ERROR_BACKTRACE, and SQLCODE into local variables as the first operation in every exception handler
any prior SQL overwrites the error context permanently.
3
RAISE_APPLICATION_ERROR with keep_errors TRUE preserves the original error chain. FALSE (the default) discards it. Use TRUE inside exception handlers, FALSE in procedural code.
4
Use PRAGMA AUTONOMOUS_TRANSACTION in every error logging procedure
without it, transaction rollbacks erase the error evidence.
5
Catch at the level where the recovery decision can be made
inner functions propagate, procedures log and re-raise, batches log and continue, API boundaries return safe messages.
6
Test every exception handler by forcing the error condition
verify the correct error code, a non-empty log entry, and batch continuation. Untested handlers fail in production.
Common mistakes to avoid
6 patterns
×
Using WHEN OTHERS THEN NULL anywhere in production code
Symptom
Errors are silently swallowed. No log entries are created. The system reports success while producing incorrect results. The failures are discovered hours or days later by customers, reconciliation processes, or downstream systems — never by the application itself.
Fix
Remove every WHEN OTHERS THEN NULL from the codebase. Replace with WHEN OTHERS that captures FORMAT_ERROR_STACK and either re-raises with RAISE (procedure level) or logs and continues (batch level). Grep the codebase: SELECT name, line, text FROM all_source WHERE type = 'PACKAGE BODY' AND UPPER(text) LIKE '%THEN NULL%' ORDER BY name, line.
×
Executing SQL before capturing FORMAT_ERROR_STACK in the exception handler
Symptom
Error log entries exist but contain empty or incorrect error stacks. The cleanup INSERT or status UPDATE executed before FORMAT_ERROR_STACK overwrote the error buffer. During incident response, the team sees log entries and concludes the errors were non-diagnostic.
Fix
Restructure every exception handler to follow the first-statement rule: capture FORMAT_ERROR_STACK, FORMAT_ERROR_BACKTRACE, and SQLCODE into local variables as the absolute first operation. Use a nested DECLARE block inside WHEN OTHERS for the local variable declarations. No SQL can precede the capture.
×
Using RAISE_APPLICATION_ERROR without the third parameter TRUE when re-raising a caught exception
Symptom
Error logs show custom error messages but no root cause information. The original ORA-XXXXX error code and its ORA-06512 call chain are permanently discarded. Post-mortem debugging can determine that an error occurred but not why.
Fix
Use TRUE as the third parameter (keep_errors) whenever RAISE_APPLICATION_ERROR is called inside an exception handler. This preserves the original error chain. Use FALSE (the default) only when raising a new error in procedural code — not inside exception handlers.
×
Using a single error code (-20000) for every custom error
Symptom
Calling code cannot distinguish validation failures from not-found errors from authorization failures. Error log analysis cannot filter by error type. Automated alerting treats every error identically. Programmatic error handling at the application layer is impossible.
Fix
Define a centralized error code scheme in a single package: -20001 to -20009 for validation, -20010 to -20019 for not-found, -20020 to -20029 for authorization. Document the scheme. Enforce through code review — no raw RAISE_APPLICATION_ERROR calls outside the error package.
×
Catching exceptions too early in inner utility functions
Symptom
Outer blocks cannot see the specific exception type because the inner function transformed it into a generic custom error. Recovery decisions that depend on the error type — returning a default for missing preferences vs. halting for missing configuration — cannot be made.
Fix
Remove exception handlers from inner utility functions unless they can genuinely recover without caller involvement. Let specific exceptions propagate to the layer that has business context to handle them. Add handlers at the procedure, batch, and API layers — not at the utility layer.
×
Error logging procedure does not use PRAGMA AUTONOMOUS_TRANSACTION
Symptom
Error log entries disappear when the main transaction rolls back. The INSERT into the error log table is part of the same transaction as the failed operation. Rollback erases the error evidence. The failure appears to have never happened.
Fix
Add PRAGMA AUTONOMOUS_TRANSACTION to the error logging procedure. Its COMMIT is independent of the caller's transaction. If the caller rolls back, the error log entry is preserved. This is the only way to ensure error evidence survives transaction failure.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01JUNIOR
What is the correct way to use WHEN OTHERS in PL/SQL?
Q02SENIOR
Why must FORMAT_ERROR_STACK be captured before any SQL in an exception h...
Q03SENIOR
What does the keep_errors parameter in RAISE_APPLICATION_ERROR do and wh...
Q04SENIOR
How do you design exception handling for a PL/SQL batch processing syste...
Q05SENIOR
What is the difference between RAISE and RAISE_APPLICATION_ERROR in an e...
Q06SENIOR
Why should the error logging procedure use PRAGMA AUTONOMOUS_TRANSACTION...
Q01 of 06JUNIOR
What is the correct way to use WHEN OTHERS in PL/SQL?
ANSWER
WHEN OTHERS has exactly two correct forms. Form one — log and re-raise: capture FORMAT_ERROR_STACK into local variables, log the error using an autonomous transaction procedure, then call RAISE to propagate the original exception. This is the standard handler for procedure-level code. Form two — log and continue: capture the error context, log it, mark the failing record as FAILED, increment an error counter, and continue to the next iteration. This is the handler for batch loop-level code. Any other form — THEN NULL, THEN RETURN, returning a default — silently swallows the exception and is never appropriate in production. WHEN OTHERS THEN NULL is the single most dangerous line of PL/SQL in a production system.
Q02 of 06SENIOR
Why must FORMAT_ERROR_STACK be captured before any SQL in an exception handler?
ANSWER
Oracle maintains exception context in a session-level buffer that is overwritten by the next SQL statement. If an INSERT, UPDATE, DELETE, COMMIT, or SELECT executes before FORMAT_ERROR_STACK is called, the buffer contains the result of that SQL operation — not the original error. The error log captures wrong or empty diagnostic information. The fix is to capture FORMAT_ERROR_STACK, FORMAT_ERROR_BACKTRACE, and SQLCODE into local variables as the absolute first operation in the handler. PL/SQL variable assignment reads the buffer without overwriting it, so the capture is safe. After capture, any SQL can execute because the error context is preserved in local variables.
Q03 of 06SENIOR
What does the keep_errors parameter in RAISE_APPLICATION_ERROR do and when should it be TRUE?
ANSWER
The third parameter — called keep_errors in Oracle documentation — controls whether the existing error stack is preserved. TRUE preserves the original error chain: the custom error is appended on top of whatever errors already exist. The caller sees both the business context and the root cause. FALSE — the default — replaces the entire stack. Only the custom error remains. Use TRUE inside exception handlers when re-raising a caught exception with additional context. Use FALSE in procedural code when raising a new error unrelated to a previously caught exception — validation failures, business rule checks, authorization denials. Using FALSE inside a WHEN OTHERS handler permanently discards the root cause.
Q04 of 06SENIOR
How do you design exception handling for a PL/SQL batch processing system?
ANSWER
Four layers. Layer one — inner functions: no exception handlers. Let specific exceptions propagate to the layer with business context. Layer two — procedure level: catch specific exceptions with context-rich handlers (NO_DATA_FOUND raises a custom not-found error, TOO_MANY_ROWS raises a diagnostic error). Catch WHEN OTHERS to log and re-raise. Layer three — batch loop level: catch WHEN OTHERS inside the loop body. Capture FORMAT_ERROR_STACK first, log the error using an autonomous transaction procedure, mark the failing record FAILED with the error context, increment an error counter, and continue to the next iteration. After the loop, check the error rate — if it exceeds a threshold (typically 5%), raise an alert. Layer four — post-batch reconciliation: compare expected vs. actual transaction counts. A batch that reports zero errors but processed fewer transactions than expected indicates a silent failure — possibly from a WHEN OTHERS THEN NULL that the audit missed.
Q05 of 06SENIOR
What is the difference between RAISE and RAISE_APPLICATION_ERROR in an exception handler?
ANSWER
RAISE with no arguments re-raises the current exception unchanged — same error code, same message, same stack. It is the simplest and safest way to propagate an exception after logging. The caller sees the original Oracle error as if the handler did not exist. RAISE_APPLICATION_ERROR creates a new exception with a custom error code (-20000 to -20999) and a custom message. With keep_errors TRUE, it appends the new error on top of the existing stack — both the custom context and the original error are visible. With keep_errors FALSE (the default), it replaces the stack entirely. Use RAISE when you want to propagate the original error unchanged. Use RAISE_APPLICATION_ERROR with TRUE when you want to add business context while preserving the root cause.
Q06 of 06SENIOR
Why should the error logging procedure use PRAGMA AUTONOMOUS_TRANSACTION?
ANSWER
Without PRAGMA AUTONOMOUS_TRANSACTION, the INSERT into the error log table is part of the same transaction as the failed operation. If the caller issues a ROLLBACK — which is common after a failed operation — the error log INSERT is also rolled back. The error evidence is destroyed. The failure appears to have never happened. With PRAGMA AUTONOMOUS_TRANSACTION, the logging procedure runs in its own independent transaction. Its COMMIT affects only the error log INSERT. The caller's subsequent ROLLBACK does not affect the log entry. This is the only way to guarantee that error evidence survives transaction failure. Every error logging procedure in a production PL/SQL system must use PRAGMA AUTONOMOUS_TRANSACTION.
01
What is the correct way to use WHEN OTHERS in PL/SQL?
JUNIOR
02
Why must FORMAT_ERROR_STACK be captured before any SQL in an exception handler?
SENIOR
03
What does the keep_errors parameter in RAISE_APPLICATION_ERROR do and when should it be TRUE?
SENIOR
04
How do you design exception handling for a PL/SQL batch processing system?
SENIOR
05
What is the difference between RAISE and RAISE_APPLICATION_ERROR in an exception handler?
SENIOR
06
Why should the error logging procedure use PRAGMA AUTONOMOUS_TRANSACTION?
SENIOR
FAQ · 6 QUESTIONS
Frequently Asked Questions
01
What is the difference between RAISE and RAISE_APPLICATION_ERROR?
RAISE with no arguments re-raises the current exception unchanged — same error code, same message, same stack. Use it when you want to propagate the original error after logging. RAISE_APPLICATION_ERROR creates a new exception with a custom code (-20000 to -20999) and message. With keep_errors TRUE, both the custom error and the original error are visible in the stack. With FALSE (default), only the custom error remains. Use RAISE for unchanged propagation. Use RAISE_APPLICATION_ERROR with TRUE for context-enriched re-raises.
Was this helpful?
02
Can I use SQLERRM instead of DBMS_UTILITY.FORMAT_ERROR_STACK?
They are not interchangeable. SQLERRM returns the error message text for the current error (or for a specified error code) but does not include the ORA-06512 stack trace. DBMS_UTILITY.FORMAT_ERROR_STACK returns the full error chain including all ORA-XXXXX codes and messages. DBMS_UTILITY.FORMAT_ERROR_BACKTRACE returns the ORA-06512 call chain with line numbers. For error logging, capture both FORMAT_ERROR_STACK and FORMAT_ERROR_BACKTRACE. SQLERRM is useful when you need the message text for display — not for diagnostic logging.
Was this helpful?
03
How do I handle errors in batch processing without halting the entire batch?
Wrap each iteration of the batch loop in its own BEGIN/EXCEPTION/END block. In the WHEN OTHERS handler: capture FORMAT_ERROR_STACK first, log the error using an autonomous transaction procedure, update the failing record's status to FAILED with the error context, increment an error counter, and let the loop continue to the next iteration. After the loop completes, check the error count against a threshold — if more than 5% of records failed, raise an alert. This pattern processes all valid records, preserves detailed error information for failures, and detects systematic issues through the threshold check.
Was this helpful?
04
Should every procedure have a WHEN OTHERS handler?
No. Inner utility functions and data access functions should generally have no WHEN OTHERS handler — they should let exceptions propagate to the calling layer that has business context to make recovery decisions. WHEN OTHERS handlers belong at three levels: procedure level (log and re-raise), batch level (log and continue), and API boundary (log and return safe message). Adding WHEN OTHERS to every function creates context-destroying checkpoints that prevent callers from seeing specific exception types.
Was this helpful?
05
How do I prevent exception handlers from hiding errors in production?
Four practices. First: remove every WHEN OTHERS THEN NULL from the codebase — grep for it and eliminate all instances. Second: enforce the first-statement rule — FORMAT_ERROR_STACK capture before any SQL in every handler. Third: use RAISE_APPLICATION_ERROR with keep_errors TRUE when re-raising caught exceptions. Fourth: use PRAGMA AUTONOMOUS_TRANSACTION in error logging procedures so log entries survive rollbacks. Enforce these practices through code review and add automated tests that force exceptions and verify the handler behavior.
Was this helpful?
06
What is PRAGMA AUTONOMOUS_TRANSACTION and why is it critical for error logging?
PRAGMA AUTONOMOUS_TRANSACTION declares that a procedure runs in its own independent transaction. Its COMMIT and ROLLBACK affect only its own operations, not the caller's transaction. For error logging, this means the INSERT into the error log table is committed independently. If the caller subsequently issues a ROLLBACK (which is common after a failed operation), the error log entry survives. Without PRAGMA AUTONOMOUS_TRANSACTION, the log INSERT is part of the caller's transaction — a rollback erases the error evidence, and the failure appears to have never occurred.