PRAGMA EXCEPTION_INIT binds a named exception to a specific Oracle error number at compile time — the binding is resolved once during compilation, not on every exception dispatch
Replaces numeric SQLCODE checks with readable exception names in WHEN clauses, making error handlers self-documenting
Key benefit: transforms opaque ORA-06512 stacks into instantly diagnosable error handling — the exception name tells you the business condition, the line number tells you where
Performance insight: named exceptions execute measurably faster than SQLCODE comparisons in tight exception-heavy loops because the match is a compile-time identity check, not a runtime integer comparison
Production insight: teams without PRAGMA EXCEPTION_INIT produce error logs that require manual ORA- number lookup on every incident — that is avoidable friction at 2 AM
Biggest mistake: declaring the pragma inside a sub-block instead of the declarative section — this causes PLS-00702 at compile time, not at runtime, which is actually the best possible failure mode
Second biggest mistake: using generic SQLCODE = -1 for all ORA-00001 variants when your schema has 15 unique constraints — this hides which constraint failed and adds hours to incident diagnosis
Plain-English First
PRAGMA EXCEPTION_INIT is Oracle's mechanism for giving a human-readable name to any Oracle error number. Instead of writing IF SQLCODE = -1403 in your exception handler — which requires every reader to know that -1403 means 'no data found' — you declare a named exception like e_no_customer and bind it to error -1403. Then you write WHEN e_no_customer, which is instantly readable by anyone on the team, including the person who just joined last week and is handling the 2 AM PagerDuty alert.
This technique has a second, less obvious benefit: it forces you to think about error conditions as named business concepts rather than numeric Oracle internals. An exception named e_order_batch_seq_unique tells you a duplicate batch sequence was detected. An exception named SQLCODE = -1 tells you nothing. That naming discipline pays dividends every time you read a log, debug a stack trace, or onboard a new engineer.
PRAGMA EXCEPTION_INIT binds a user-defined exception name to a specific Oracle error number at compile time. This eliminates numeric SQLCODE comparisons in exception handlers and produces code that reads like intent rather than implementation. The pragma is a directive to the PL/SQL compiler — it has zero runtime overhead for the binding itself, only the normal exception dispatch cost.
Production systems with extensive error handling benefit most from this pattern. When an ORA-06512 stack trace appears in your monitoring at 3 AM, a named exception immediately tells the on-call engineer which business condition failed — without opening the Oracle error reference or mentally decoding negative integers. That 30-second reduction in time-to-understand compounds across thousands of incidents per year.
The pragma must appear in the declarative section of a PL/SQL block, after the exception name declaration and before the BEGIN keyword. This is a hard compiler rule, not a style preference. Placement errors produce PLS-00702 at compilation, which is the best possible failure mode — caught before deployment, never in production.
This guide covers the syntax mechanics, a production-grade centralized exception package pattern, constraint-specific exception mapping, dynamic fallback with GET STACKED DIAGNOSTICS, and a complete test suite for validating pragma declarations across Oracle upgrades.
PRAGMA EXCEPTION_INIT Syntax and Mechanics
PRAGMA EXCEPTION_INIT requires two arguments in a specific order: the declared exception name first, then the negative Oracle error number. This ordering trips up developers who write PRAGMA EXCEPTION_INIT(-1403, e_my_exception) — the reversed syntax compiles in some contexts but silently produces incorrect behavior.
The declaration must appear in the declarative section of any PL/SQL block: anonymous blocks, named procedures, functions, package specifications, package bodies, and triggers all support it. The exception name must be declared in the same declarative section before the pragma references it — forward references are not permitted.
The bound error number must be negative. Oracle error numbers are negative integers by convention — ORA-01403 is -1403, ORA-00001 is -1. Using a positive value (1403 instead of -1403) is not a compilation error, but the binding silently fails. The named exception will never fire, and errors will fall through to WHEN OTHERS. This is the most dangerous failure mode of PRAGMA EXCEPTION_INIT because it produces no error at compile time or runtime — it simply does not work.
One exception name maps to exactly one error number. Multiple exceptions in the same block can each map to different error numbers. The compiler resolves all bindings at compilation time and embeds them in the compiled object — there is no lookup at runtime when an exception is raised.
pragma_syntax.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
-- ===================================================-- CORRECT PATTERNS-- ===================================================-- Pattern 1: Basic anonymous block usageDECLARE
e_no_customer EXCEPTION;
PRAGMA EXCEPTION_INIT(e_no_customer, -1403); -- ORA-01403: no data found
v_customer_name VARCHAR2(200);
BEGINSELECT customer_name INTO v_customer_name
FROM io.thecodeforge.customers
WHERE customer_id = p_customer_id;
RETURN v_customer_name;
EXCEPTIONWHEN e_no_customer THEN-- Instantly readable: customer not found, return NULL
io.thecodeforge.logging.log_warning(
'CUSTOMER_LOOKUP',
'Customer ID ' || p_customer_id || ' not found — returning NULL'
);
RETURNNULL;
END;
/
-- Pattern 2: Multiple exceptions in the same declarative sectionDECLARE
e_dup_order EXCEPTION;
PRAGMA EXCEPTION_INIT(e_dup_order, -1); -- ORA-00001: unique violation
e_parent_missing EXCEPTION;
PRAGMA EXCEPTION_INIT(e_parent_missing, -2291); -- ORA-02291: FK violation
e_deadlock EXCEPTION;
PRAGMA EXCEPTION_INIT(e_deadlock, -60); -- ORA-00060: deadlockBEGININSERTINTO io.thecodeforge.orders (
order_id, customer_id, order_date
) VALUES (
p_order_id, p_customer_id, SYSDATE
);
EXCEPTIONWHEN e_dup_order THEN
io.thecodeforge.logging.log_warning('ORDER_INSERT', 'Duplicate order: ' || p_order_id);
WHEN e_parent_missing THEN
io.thecodeforge.logging.log_error('ORDER_INSERT', 'Customer ' || p_customer_id || ' not found');
RAISE_APPLICATION_ERROR(-20001, 'Invalid customer ID: ' || p_customer_id);
WHEN e_deadlock THEN-- Deadlock: roll back and signal caller to retryROLLBACK;
RAISE_APPLICATION_ERROR(-20099, 'Deadlock on order insert — retry eligible');
END;
/
-- ===================================================-- WRONG PATTERNS — DO NOT SHIP THESE-- ===================================================-- WRONG 1: Pragma after BEGIN — causes PLS-00702 at compile timeDECLARE
e_my_exception EXCEPTION;
BEGINPRAGMA EXCEPTION_INIT(e_my_exception, -1403); -- COMPILATION ERRORSELECT customer_name INTO v_name FROM customers WHERE customer_id = p_id;
END;
/
-- WRONG 2: Positive error number — silent failure, exception never firesDECLARE
e_my_exception EXCEPTION;
PRAGMA EXCEPTION_INIT(e_my_exception, 1403); -- WRONG: must be -1403BEGINNULL;
EXCEPTIONWHEN e_my_exception THENNULL; -- This handler will NEVER executeEND;
/
-- WRONG 3: Reversed argument order — compiles but binding is incorrectDECLARE
e_my_exception EXCEPTION;
PRAGMA EXCEPTION_INIT(-1403, e_my_exception); -- WRONG argument orderBEGINNULL;
END;
/
-- WRONG 4: Forward reference — exception not yet declaredDECLAREPRAGMA EXCEPTION_INIT(e_my_exception, -1403); -- WRONG: declared before exception
e_my_exception EXCEPTION;
BEGINNULL;
END;
/
Pragma as Compile-Time Binding — Not Runtime Logic
The binding is resolved once at compilation — zero runtime overhead for the lookup itself
One exception name maps to exactly one error number — there is no range binding or wildcard
Multiple exceptions can coexist in the same declarative section, each bound to a different error number
The exception name becomes a first-class citizen in WHEN clauses — no SQLCODE comparison needed
If you re-raise with RAISE, the original Oracle error number is preserved in the stack
If you use RAISE_APPLICATION_ERROR after catching, the original Oracle error is replaced — log FORMAT_ERROR_STACK first
Production Insight
Pragma placement errors (PLS-00702) are caught at compile time — this is the best possible failure mode.
Positive error number errors are not caught at compile time — they are silent failures that require a unit test to detect.
Always verify pragma declarations with a deliberate error-raising test, not just a compilation check.
In Oracle 19c+, the PL/SQL compiler includes pragma validation warnings — enable PLSQL_WARNINGS = 'ENABLE:ALL' in your development environment to catch subtle binding issues during compilation rather than at runtime.
Key Takeaway
Pragma binds exception name to error number at compile time — zero runtime overhead for the binding.
Argument order matters: exception name first, negative error number second.
Always use negative error numbers — positive values compile without error but the binding silently fails.
Validate every pragma with a unit test that deliberately raises the bound error.
Where to Declare Your PRAGMA EXCEPTION_INIT
IfException used only in one procedure or function
→
UseDeclare in that procedure's declarative section — keep it local, keep it close to where it is used
IfException used across multiple procedures within a single package
→
UseDeclare in the package specification — visible to all procedures and functions in that package and to external callers
IfException used across multiple packages in the same schema
→
UseCreate io.thecodeforge.error_pkg with all declarations in the specification — single source of truth for the entire schema
IfException used across schemas or in a shared services layer
→
UsePlace error_pkg in a shared utility schema with explicit grants to application schemas — reference as shared_schema.error_pkg.exception_name
IfError number behavior differs between Oracle versions in your upgrade path
→
UseAdd a version comment to the pragma declaration noting the verified Oracle version. Run the test suite after every patch. Oracle error numbers are stable but sub-errors within ORA-00600 internal errors can shift.
Centralized Exception Package Pattern
Production systems with dozens of packages and hundreds of procedures need a centralized exception declaration package. Without it, the same error number gets re-declared under different names in different packages, developers disagree on whether -1403 should be e_no_data_found or e_not_found or e_missing_record, and error log searches require knowing which package used which name.
The centralized exception package pattern solves this by declaring every named exception in a single package specification. Other packages and procedures reference these exceptions without redeclaring them, using the fully qualified package_name.exception_name syntax. This pattern has three measurable benefits: new engineers understand error conditions by reading exception names instead of looking up error numbers; error log grep searches become deterministic because the same condition always uses the same name; and adding a new exception or modifying an existing one requires a change in exactly one location.
The package below is a production-ready starting point. Extend it with constraint-specific exceptions (covered in the next section) and custom application error ranges specific to your business domain.
centralized_error_pkg.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
154
155
156
157
158
159
160
161
162
163
-- ===================================================-- io.thecodeforge.error_pkg — Package Specification-- Single source of truth for all named exceptions.-- Reference from other packages as:-- io.thecodeforge.error_pkg.e_unique_violation-- ===================================================CREATEORREPLACEPACKAGE io.thecodeforge.error_pkg AS-- ---------------------------------------------------- DATA INTEGRITY EXCEPTIONS-- --------------------------------------------------
e_unique_violation EXCEPTION; -- ORA-00001PRAGMA EXCEPTION_INIT(e_unique_violation, -1);
e_fk_parent_not_found EXCEPTION; -- ORA-02291PRAGMA EXCEPTION_INIT(e_fk_parent_not_found, -2291);
e_fk_child_exists EXCEPTION; -- ORA-02292PRAGMA EXCEPTION_INIT(e_fk_child_exists, -2292);
e_not_null_violation EXCEPTION; -- ORA-01400PRAGMA EXCEPTION_INIT(e_not_null_violation, -1400);
e_value_too_large EXCEPTION; -- ORA-12899PRAGMA EXCEPTION_INIT(e_value_too_large, -12899);
e_check_constraint EXCEPTION; -- ORA-02290PRAGMA EXCEPTION_INIT(e_check_constraint, -2290);
-- ---------------------------------------------------- QUERY AND CURSOR EXCEPTIONS-- --------------------------------------------------
e_no_data_found EXCEPTION; -- ORA-01403PRAGMA EXCEPTION_INIT(e_no_data_found, -1403);
e_too_many_rows EXCEPTION; -- ORA-01422PRAGMA EXCEPTION_INIT(e_too_many_rows, -1422);
e_cursor_already_open EXCEPTION; -- ORA-06511PRAGMA EXCEPTION_INIT(e_cursor_already_open, -6511);
e_max_open_cursors EXCEPTION; -- ORA-01000PRAGMA EXCEPTION_INIT(e_max_open_cursors, -1000);
-- ---------------------------------------------------- TYPE CONVERSION EXCEPTIONS-- --------------------------------------------------
e_invalid_number EXCEPTION; -- ORA-01722PRAGMA EXCEPTION_INIT(e_invalid_number, -1722);
e_value_error EXCEPTION; -- ORA-06502PRAGMA EXCEPTION_INIT(e_value_error, -6502);
e_date_format_mismatch EXCEPTION; -- ORA-01830PRAGMA EXCEPTION_INIT(e_date_format_mismatch, -1830);
e_invalid_month EXCEPTION; -- ORA-01843PRAGMA EXCEPTION_INIT(e_invalid_month, -1843);
-- ---------------------------------------------------- CONCURRENCY AND LOCKING EXCEPTIONS-- --------------------------------------------------
e_deadlock EXCEPTION; -- ORA-00060PRAGMA EXCEPTION_INIT(e_deadlock, -60);
e_resource_busy EXCEPTION; -- ORA-00054PRAGMA EXCEPTION_INIT(e_resource_busy, -54);
e_snapshot_too_old EXCEPTION; -- ORA-01555PRAGMA EXCEPTION_INIT(e_snapshot_too_old, -1555);
-- ---------------------------------------------------- MEMORY AND RESOURCE EXCEPTIONS-- --------------------------------------------------
e_out_of_process_memory EXCEPTION; -- ORA-04030PRAGMA EXCEPTION_INIT(e_out_of_process_memory, -4030);
e_shared_pool_exhausted EXCEPTION; -- ORA-04031PRAGMA EXCEPTION_INIT(e_shared_pool_exhausted, -4031);
e_package_state_discarded EXCEPTION; -- ORA-04068PRAGMA EXCEPTION_INIT(e_package_state_discarded, -4068);
-- ---------------------------------------------------- COLLECTION EXCEPTIONS-- --------------------------------------------------
e_collection_uninit EXCEPTION; -- ORA-06531PRAGMA EXCEPTION_INIT(e_collection_uninit, -6531);
e_subscript_beyond_count EXCEPTION; -- ORA-06533PRAGMA EXCEPTION_INIT(e_subscript_beyond_count, -6533);
-- ---------------------------------------------------- CUSTOM APPLICATION EXCEPTIONS (-20000 to -20999)-- Reserve ranges for specific subsystems:-- -20001 to -20099: business rule violations-- -20100 to -20199: data quality errors-- -20200 to -20299: configuration errors-- -20300 to -20399: integration errors-- --------------------------------------------------
e_business_rule_violation EXCEPTION; -- ORA-20001PRAGMA EXCEPTION_INIT(e_business_rule_violation, -20001);
e_data_quality_error EXCEPTION; -- ORA-20101PRAGMA EXCEPTION_INIT(e_data_quality_error, -20101);
e_configuration_error EXCEPTION; -- ORA-20201PRAGMA EXCEPTION_INIT(e_configuration_error, -20201);
e_integration_error EXCEPTION; -- ORA-20301PRAGMA EXCEPTION_INIT(e_integration_error, -20301);
END io.thecodeforge.error_pkg;
/
-- ===================================================-- Usage example: referencing error_pkg from another package-- ===================================================CREATEORREPLACEPROCEDURE io.thecodeforge.order.create_order(
p_customer_id INNUMBER,
p_order_id OUTNUMBER
) ISBEGININSERTINTO io.thecodeforge.orders (
customer_id, order_date, status, created_at
) VALUES (
p_customer_id, SYSDATE, 'PENDING', SYSTIMESTAMP
) RETURNING order_id INTO p_order_id;
COMMIT;
EXCEPTIONWHEN io.thecodeforge.error_pkg.e_fk_parent_not_found THEN-- Instantly readable: the customer_id FK reference is invalidROLLBACK;
io.thecodeforge.logging.log_error(
'ORDER_CREATE',
'Customer ' || p_customer_id || ' does not exist in customers table'
);
RAISE_APPLICATION_ERROR(
-20001,
'Cannot create order: customer ID ' || p_customer_id || ' not found'
);
WHEN io.thecodeforge.error_pkg.e_unique_violation THEN-- Unique constraint: which one? Extract from SQLERRMROLLBACK;
io.thecodeforge.logging.log_error(
'ORDER_CREATE',
'Duplicate order detected. Customer: ' || p_customer_id ||
' Stack: ' || DBMS_UTILITY.FORMAT_ERROR_STACK
);
RAISE; -- Let the caller decide how to handle duplicatesWHEN io.thecodeforge.error_pkg.e_deadlock THEN-- Deadlock: always roll back and signal retry-eligibleROLLBACK;
RAISE_APPLICATION_ERROR(
-20099,
'Deadlock on order create for customer ' || p_customer_id ||
' — retry eligible'
);
END create_order;
/
Centralized Package — Operational Considerations
Grant EXECUTE on error_pkg to all application schemas that reference it — missing grants cause PLS-00201 at compile time of dependent packages
Any change to error_pkg specification triggers cascading recompilation of all dependent packages — plan deployments accordingly and recompile dependents before releasing to production
In Oracle RAC environments, verify error_pkg compiles identically on all nodes — object metadata is shared but compilation state can diverge if nodes are patched independently
Add a version constant to error_pkg (e.g., PKG_VERSION CONSTANT VARCHAR2(10) := '2.3.0') so you can verify the correct version is deployed without querying DBA_OBJECTS timestamps
Document the custom error number range allocation in the package header — undocumented ranges cause conflicts when different teams use the same -200XX number for different business conditions
Production Insight
Centralized error packages reduce incident diagnosis time significantly for teams that previously used scattered SQLCODE comparisons.
New engineers read exception names instead of looking up error numbers — and they contribute correct error handling from their first week.
Error log grep searches become deterministic: grep for e_fk_parent_not_found finds every foreign key violation across all packages, regardless of who wrote the code.
Since any spec change cascades recompilation to dependent packages, treat error_pkg like a public API — additions are safe, modifications to existing exception names require a coordinated deploy.
Key Takeaway
Centralize all PRAGMA EXCEPTION_INIT declarations in one package specification.
Reference exceptions via io.thecodeforge.error_pkg.exception_name — fully qualified, unambiguous.
Add a version constant and a documented custom error range allocation.
Treat error_pkg like a public API — additions are safe, renames require coordinated deploys.
Constraint-Specific Exception Mapping
Generic ORA-00001 handlers are the most common source of undiagnosable constraint violations in production. When your schema has 15 unique constraints across 8 tables and your handler catches a generic e_unique_violation, you know a duplicate was rejected — but you do not know which constraint, which table, which column, or which business condition was violated. That missing context is the difference between a 5-minute incident resolution and a 3-hour manual investigation.
The solution is to map each unique constraint to its own named exception. Since all unique constraint violations share the same Oracle error number (-1), you cannot distinguish them by error number alone. Instead, declare separate named exceptions (all bound to -1) and use a WHEN clause for each specific constraint in procedures that can violate multiple constraints. In each handler, you know the business context because the handler name tells you which constraint is being handled.
The caveat: in a single EXCEPTION block, you cannot have two WHEN clauses for exceptions bound to the same error number and expect Oracle to route selectively — Oracle fires the first matching WHEN clause. The constraint-specific naming value comes from having separate procedures for separate business operations, each with its own exception handler for the constraints that operation can violate. Where one procedure can violate multiple constraints, fall back to extracting the constraint name from SQLERRM.
constraint_specific_exceptions.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
-- ===================================================-- Step 1: Generate exception declarations from data dictionary-- Run this to produce the code for your error_pkg-- ===================================================SELECT
' -- Table: ' || table_name || ' | Type: ' ||CASE constraint_type
WHEN'U'THEN'UNIQUE'WHEN'P'THEN'PRIMARY KEY'WHEN'R'THEN'FOREIGN KEY'ENDAS comment_line,
' e_' || LOWER(REGEXP_REPLACE(constraint_name, '[^A-Z0-9]', '_')) ||
' EXCEPTION; -- ' || table_name AS exception_decl,' PRAGMA EXCEPTION_INIT(e_' ||
LOWER(REGEXP_REPLACE(constraint_name, '[^A-Z0-9]', '_')) ||
', ' ||
CASE constraint_type
WHEN'U'THEN'-1'WHEN'P'THEN'-1'WHEN'R'THEN'-2291'END || ');'AS pragma_decl
FROM user_constraints
WHERE constraint_type IN ('U', 'P', 'R')
AND generated = 'USERNAME' -- exclude system-generated namesORDERBY table_name, constraint_type, constraint_name;
-- ===================================================-- Step 2: Constraint-specific exceptions in error_pkg-- Example: payment system schema-- ===================================================CREATEORREPLACEPACKAGE io.thecodeforge.error_pkg AS-- Generic unique violation (catch-all for unspecified constraints)
e_unique_violation EXCEPTION;
PRAGMA EXCEPTION_INIT(e_unique_violation, -1);
-- io.thecodeforge.payment.transaction_log-- Unique index on (batch_id, sequence_number)
e_txn_batch_seq_unique EXCEPTION;
PRAGMA EXCEPTION_INIT(e_txn_batch_seq_unique, -1);
-- io.thecodeforge.customers-- Unique index on (email_address)
e_customer_email_unique EXCEPTION;
PRAGMA EXCEPTION_INIT(e_customer_email_unique, -1);
-- Unique index on (tax_id_number)
e_customer_tax_id_unique EXCEPTION;
PRAGMA EXCEPTION_INIT(e_customer_tax_id_unique, -1);
-- io.thecodeforge.orders-- Unique index on (external_reference_id)
e_order_ext_ref_unique EXCEPTION;
PRAGMA EXCEPTION_INIT(e_order_ext_ref_unique, -1);
-- FK exceptions (distinct error numbers — can be caught selectively)
e_txn_order_fk EXCEPTION; -- transaction -> order FKPRAGMA EXCEPTION_INIT(e_txn_order_fk, -2291);
e_order_customer_fk EXCEPTION; -- order -> customer FKPRAGMA EXCEPTION_INIT(e_order_customer_fk, -2291);
END io.thecodeforge.error_pkg;
/
-- ===================================================-- Step 3: Procedure with constraint-specific handlers-- Each handler communicates the exact business condition-- ===================================================CREATEORREPLACEPROCEDURE io.thecodeforge.payment.insert_transaction(
p_batch_id INNUMBER,
p_seq_number INNUMBER,
p_order_id INNUMBER,
p_amount INNUMBER
) IS
v_constraint_name VARCHAR2(128);
BEGININSERTINTO io.thecodeforge.payment.transaction_log (
batch_id, sequence_number, order_id, amount, created_at
) VALUES (
p_batch_id, p_seq_number, p_order_id, p_amount, SYSTIMESTAMP
);
EXCEPTIONWHEN io.thecodeforge.error_pkg.e_txn_order_fk THEN-- FK: order does not exist — this is always a data quality issue
io.thecodeforge.logging.log_error(
'TXN_INSERT',
'Order ' || p_order_id || ' not found. Batch: ' || p_batch_id ||
' Seq: ' || p_seq_number
);
RAISE_APPLICATION_ERROR(-20301,
'Integration error: order ID ' || p_order_id ||
' does not exist in orders table');
WHEN io.thecodeforge.error_pkg.e_unique_violation THEN-- Unique violation: extract constraint name for diagnosis-- (since all unique exceptions share -1, we use SQLERRM here)
v_constraint_name := REGEXP_SUBSTR(
SQLERRM,
'\(([^.]+)\.([^)]+)\)',
1, 1, NULL, 2-- capture group 2: constraint name after schema
);
IF v_constraint_name = 'TXN_BATCH_SEQ_UK'THEN-- Known duplicate: batch/sequence already processed
io.thecodeforge.logging.log_warning(
'TXN_DUPLICATE',
'Batch ' || p_batch_id || ' seq ' || p_seq_number ||
' already exists — idempotent skip'
);
-- Do not raise: idempotent skip is correct behavior for known duplicatesELSE-- Unknown constraint: log fully and raise for investigation
io.thecodeforge.logging.log_error(
'TXN_UNEXPECTED_DUP',
'Constraint: ' || NVL(v_constraint_name, 'UNKNOWN') ||
' Batch: ' || p_batch_id || ' Seq: ' || p_seq_number ||
' Stack: ' || DBMS_UTILITY.FORMAT_ERROR_STACK
);
RAISE;
ENDIF;
END insert_transaction;
/
-- ===================================================-- Step 4: Audit query — find all constraint violations-- that hit the catch-all e_unique_violation handler-- (these are candidates for constraint-specific mapping)-- ===================================================SELECT constraint_name_extracted,
COUNT(*) AS occurrences,
MAX(created_at) AS last_seen
FROM (
SELECT
REGEXP_SUBSTR(
full_stack,
'\(IO\.THECODEFORGE\.([^)]+)\)',
1, 1, NULL, 1
) AS constraint_name_extracted,
created_at
FROM io.thecodeforge.logging.error_log
WHERE primary_error_code = 'ORA-00001'AND created_at > SYSDATE - 30
)
GROUPBY constraint_name_extracted
ORDERBY occurrences DESC;
Constraint Exception Strategy — The Key Limitation to Understand
All unique constraint violations share ORA-00001 (-1) — you cannot have Oracle route selectively to different handlers in the same EXCEPTION block based on which constraint fired
The value of constraint-specific exception names is in procedure design: each procedure that can only violate one specific constraint gets one specific named handler
For procedures that can violate multiple constraints, catch the generic e_unique_violation and use REGEXP_SUBSTR on SQLERRM to extract the constraint name
FK violations (ORA-02291) are better for selective catching because different FK constraints point to different parent tables — each one is genuinely a different error condition
Use the audit query above to identify which constraint names appear most in your error logs — prioritize those for constraint-specific procedure design
Generate declarations from USER_CONSTRAINTS after every schema migration — constraint names drift faster than you think
Production Insight
Constraint-specific exception names provide their value at code-reading time, not at exception-dispatch time. When a developer reads WHEN e_txn_batch_seq_unique THEN, they know exactly what failed. When they read WHEN e_unique_violation THEN, they need to understand the procedure's context to know which constraint it implies.
During incidents, the exception name in the log is the first piece of information the on-call engineer has before they even look at the stack trace.
Generate declarations from USER_CONSTRAINTS as part of your schema migration tooling — not as a manual step. Constraints added during a sprint that are not in error_pkg create silent generic-catch scenarios.
Key Takeaway
Constraint-specific exception names communicate business intent at code-reading time.
All unique violations share -1 — selective dispatch within one EXCEPTION block is not possible.
Design procedures so each one can only violate one constraint, then the handler is unambiguous.
Generate declarations from USER_CONSTRAINTS — never enumerate constraints manually at scale.
Constraint Exception Naming Strategy
IfProcedure inserts into one table with one unique constraint
→
UseDeclare a constraint-specific exception. The WHEN clause is unambiguous and self-documenting.
IfProcedure inserts into one table with multiple unique constraints
→
UseCatch e_unique_violation and extract constraint name from SQLERRM using REGEXP_SUBSTR — then branch with IF/ELSIF on constraint name
IfProcedure inserts into multiple tables across a transaction
→
UseWrap each INSERT in its own sub-block with its own handler. Each sub-block catches only the constraints for its specific table.
IfFK violations across multiple parent tables
→
UseDeclare separate named exceptions for each FK — they all share -2291 but each represents a different missing parent entity
IfSchema has more than 30 unique constraints
→
UseGenerate declarations from USER_CONSTRAINTS. Manual enumeration at this scale will drift. Automate with a DDL trigger or migration script.
Dynamic Exception Handling with GET STACKED DIAGNOSTICS
PRAGMA EXCEPTION_INIT handles compile-time known errors elegantly, but two scenarios require runtime error extraction: dynamic SQL where the statement structure is built at runtime, and cross-environment deployments where constraint names differ between development, staging, and production.
GET STACKED DIAGNOSTICS is the runtime complement to PRAGMA EXCEPTION_INIT. It provides structured access to the current exception's metadata — error message, SQLSTATE code, constraint name (in some contexts), and schema object identifiers — without string parsing. Unlike SQLERRM (which Oracle has left largely unchanged since Oracle 7), GET STACKED DIAGNOSTICS is part of the SQL/PSM standard and provides cleaner structured output.
Note that GET STACKED DIAGNOSTICS is the PostgreSQL/standard syntax. In Oracle PL/SQL, you use DBMS_UTILITY.FORMAT_ERROR_STACK, FORMAT_ERROR_BACKTRACE, and SQLERRM for the equivalent functionality. The hybrid pattern below uses named exceptions for known errors and SQLERRM/FORMAT_ERROR_STACK parsing for dynamic cases. This is the correct production approach — not one or the other, but both working together.
dynamic_exception_handling.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
-- ===================================================-- Hybrid pattern: PRAGMA EXCEPTION_INIT for known errors-- + SQLERRM/FORMAT_ERROR_STACK parsing for dynamic cases-- ===================================================CREATEORREPLACEPROCEDURE io.thecodeforge.etl.load_staging_data(
p_source_table INVARCHAR2, -- dynamic: table name varies by feed
p_target_table INVARCHAR2
) IS
v_sql VARCHAR2(4000);
v_constraint_name VARCHAR2(128);
v_error_stack VARCHAR2(4000);
v_row_count NUMBER := 0;
v_error_count NUMBER := 0;
-- Known errors: use named exceptions
e_source_not_found EXCEPTION;
PRAGMA EXCEPTION_INIT(e_source_not_found, -942); -- ORA-00942: table not found
e_target_not_found EXCEPTION;
PRAGMA EXCEPTION_INIT(e_target_not_found, -942); -- same error, different semanticBEGIN-- Validate that both tables exist before building dynamic SQLDECLARE
v_exists NUMBER;
BEGINSELECTCOUNT(*) INTO v_exists
FROM user_tables
WHERE table_name = UPPER(p_source_table);
IF v_exists = 0THEN
RAISE_APPLICATION_ERROR(-20301,
'Source table does not exist: ' || p_source_table);
ENDIF;
END;
-- Build and execute dynamic SQL for the load
v_sql := 'INSERT INTO io.thecodeforge.' || p_target_table ||
' SELECT * FROM io.thecodeforge.staging.' || p_source_table ||
' WHERE status = ''PENDING''';
BEGINEXECUTEIMMEDIATE v_sql;
v_row_count := SQL%ROWCOUNT;
EXCEPTIONWHEN io.thecodeforge.error_pkg.e_unique_violation THEN-- Known error type, unknown which constraint-- Extract constraint name from SQLERRM at runtime
v_error_stack := DBMS_UTILITY.FORMAT_ERROR_STACK;
v_constraint_name := REGEXP_SUBSTR(
v_error_stack,
'unique constraint \(([^.]+)\.([^)]+)\)',
1, 1, 'i', 2-- capture group 2: constraint name
);
io.thecodeforge.logging.log_error(
'ETL_UNIQUE_VIOLATION',
'Table: ' || p_target_table ||
' Constraint: ' || NVL(v_constraint_name, 'UNKNOWN') ||
' SQL: ' || SUBSTR(v_sql, 1, 500)
);
v_error_count := v_error_count + 1;
-- Do not re-raise for batch loads — log and continueWHEN io.thecodeforge.error_pkg.e_snapshot_too_old THEN-- ORA-01555: long-running insert conflicted with undo
io.thecodeforge.logging.log_error(
'ETL_SNAPSHOT_TOO_OLD',
'Table: ' || p_target_table ||
' SQL: ' || SUBSTR(v_sql, 1, 500) ||
' Action: Increase UNDO_RETENTION or reduce batch window'
);
RAISE; -- This one we DO re-raise — cannot continueWHENOTHERSTHEN-- Unknown error: capture everything for diagnosis
v_error_stack := DBMS_UTILITY.FORMAT_ERROR_STACK;
io.thecodeforge.logging.log_error(
'ETL_UNKNOWN_ERROR',
'SQLCODE: ' || SQLCODE || CHR(10) ||
'Table: ' || p_target_table || CHR(10) ||
'SQL: ' || SUBSTR(v_sql, 1, 500) || CHR(10) ||
'Stack: ' || v_error_stack || CHR(10) ||
'Backtrace: ' || DBMS_UTILITY.FORMAT_ERROR_BACKTRACE
);
RAISE;
END;
COMMIT;
io.thecodeforge.logging.log_info(
'ETL_LOAD_COMPLETE',
'Table: ' || p_target_table ||
' Rows loaded: ' || v_row_count ||
' Errors: ' || v_error_count
);
END load_staging_data;
/
-- ===================================================-- Pattern: parse SQLERRM to route dynamically-- Use when the same procedure handles multiple-- constraint types and named exceptions cannot differentiate-- ===================================================CREATEORREPLACEPROCEDURE io.thecodeforge.etl.smart_error_router IS
v_error_code NUMBER;
v_error_msg VARCHAR2(4000);
v_constraint VARCHAR2(128);
BEGIN-- ... DML operations ...NULL;
EXCEPTIONWHENOTHERSTHEN
v_error_code := SQLCODE;
v_error_msg := DBMS_UTILITY.FORMAT_ERROR_STACK;
CASE v_error_code
WHEN -1THEN-- ORA-00001: unique violation
v_constraint := REGEXP_SUBSTR(
v_error_msg, 'unique constraint \([^.]+\.([^)]+)\)', 1, 1, 'i', 1
);
io.thecodeforge.logging.log_error(
'UNIQUE_VIOLATION',
'Constraint: ' || NVL(v_constraint, 'UNKNOWN') ||
' Full stack: ' || v_error_msg
);
WHEN -2291THEN-- ORA-02291: FK parent not found
io.thecodeforge.logging.log_error(
'FK_VIOLATION',
'Parent key not found: ' || v_error_msg
);
ELSE
io.thecodeforge.logging.log_error(
'UNEXPECTED_ERROR',
'Code: ' || v_error_code ||
' Stack: ' || v_error_msg ||
' Backtrace: ' || DBMS_UTILITY.FORMAT_ERROR_BACKTRACE
);
ENDCASE;
RAISE; -- Always re-raise after loggingEND smart_error_router;
/
When PRAGMA EXCEPTION_INIT Cannot Cover Everything
Dynamic SQL (EXECUTE IMMEDIATE) is the primary scenario where runtime error extraction is necessary — you cannot declare a named exception for every possible constraint in a dynamically targeted table
DBMS_UTILITY.FORMAT_ERROR_STACK is Oracle's equivalent of GET STACKED DIAGNOSTICS — use it for the full error text up to 2000 bytes
DBMS_UTILITY.FORMAT_ERROR_BACKTRACE preserves original line numbers through re-raises — always log this alongside FORMAT_ERROR_STACK
REGEXP_SUBSTR on SQLERRM is fragile across Oracle versions — the error message format can change. Test your regex after every major Oracle upgrade
The hybrid pattern (named exceptions for known errors + SQLERRM parsing for dynamic fallback) is more robust than either approach alone
Always log the dynamic SQL text alongside the error — the ORA-06512 line number points to EXECUTE IMMEDIATE, which is useless without the generated SQL
Production Insight
Dynamic SQL error handling is where the most information gets lost in production systems.
The ORA-06512 stack says 'line 47' — which is the EXECUTE IMMEDIATE line, not the failing SQL.
Always capture the SQL string in a variable before executing, and include SUBSTR(v_sql, 1, 500) in every error log for dynamic SQL procedures.
For ETL systems that process multiple tables dynamically, log the table name, row count processed, and error count as a completion record even on partial success — silent partial failures are worse than explicit failures.
Key Takeaway
PRAGMA EXCEPTION_INIT covers compile-time known errors elegantly.
DBMS_UTILITY.FORMAT_ERROR_STACK and SQLERRM parsing cover dynamic SQL and runtime-unknown errors.
Always log the dynamic SQL text alongside the error — the line number alone is not diagnostic.
Use both approaches together: named exceptions for known conditions, SQLERRM parsing as the fallback.
Testing PRAGMA EXCEPTION_INIT Declarations
Every PRAGMA EXCEPTION_INIT declaration needs a corresponding unit test that deliberately raises the bound error and verifies the named exception fires — not WHEN OTHERS. Without this test, silent failures are invisible: a positive error number that was accidentally used instead of negative compiles cleanly, runs without error in normal code paths, and only fails when an actual exception occurs in production.
The test pattern is simple: create conditions that will raise the specific Oracle error, catch the named exception, and set a boolean flag. If the flag is not set after the test block, the named exception did not fire — which means the binding is broken.
Three events require re-running the pragma test suite: Oracle version upgrades (error numbers are stable but some internal sub-codes shift), schema DDL changes (new or renamed constraints invalidate constraint-specific exception names), and error_pkg deployments (to confirm the newly compiled package binds correctly before dependent packages go live).
pragma_test_suite.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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
-- ===================================================-- io.thecodeforge.test.error_pkg_test-- Complete test suite for all PRAGMA EXCEPTION_INIT-- Run after: Oracle upgrades, schema DDL, error_pkg deploys-- ===================================================CREATEORREPLACEPACKAGE io.thecodeforge.test.error_pkg_test ASPROCEDURE test_unique_violation_mapping;
PROCEDURE test_fk_parent_not_found_mapping;
PROCEDURE test_fk_child_exists_mapping;
PROCEDURE test_no_data_found_mapping;
PROCEDURE test_snapshot_too_old_is_declared; -- cannot artificially raise, just verify declarationPROCEDURE test_deadlock_is_declared;
PROCEDURE test_custom_error_range_mapping;
PROCEDURE run_all_tests;
END io.thecodeforge.test.error_pkg_test;
/
CREATEORREPLACEPACKAGEBODY io.thecodeforge.test.error_pkg_test AS-- Internal helper: fail with a clear messagePROCEDUREfail(p_test_name INVARCHAR2, p_message INVARCHAR2) ISBEGIN
RAISE_APPLICATION_ERROR(-20999,
'PRAGMA TEST FAIL: ' || p_test_name || ' — ' || p_message);
END fail;
PROCEDUREpass(p_test_name INVARCHAR2) ISBEGIN
DBMS_OUTPUT.PUT_LINE('PASS: ' || p_test_name);
END pass;
-- ---------------------------------------------------- Test: e_unique_violation catches ORA-00001-- --------------------------------------------------PROCEDURE test_unique_violation_mapping IS
v_caught BOOLEAN := FALSE;
BEGIN-- Ensure test customer existsBEGININSERTINTO io.thecodeforge.test.test_customers (
customer_id, email
) VALUES (999999, 'pragma_test@thecodeforge.io');
EXCEPTIONWHEN io.thecodeforge.error_pkg.e_unique_violation THENNULL; -- Already exists from a previous run — that is fineEND;
-- Now deliberately trigger ORA-00001BEGININSERTINTO io.thecodeforge.test.test_customers (
customer_id, email
) VALUES (999999, 'pragma_test_dup@thecodeforge.io'); -- duplicate PKEXCEPTIONWHEN io.thecodeforge.error_pkg.e_unique_violation THEN
v_caught := TRUE;
WHENOTHERSTHENfail('test_unique_violation_mapping',
'WHEN OTHERS caught instead of e_unique_violation. SQLCODE: ' || SQLCODE);
END;
ROLLBACK;
IFNOT v_caught THENfail('test_unique_violation_mapping',
'e_unique_violation did not fire for ORA-00001 — check pragma binding');
ENDIF;
pass('test_unique_violation_mapping');
END test_unique_violation_mapping;
-- ---------------------------------------------------- Test: e_fk_parent_not_found catches ORA-02291-- --------------------------------------------------PROCEDURE test_fk_parent_not_found_mapping IS
v_caught BOOLEAN := FALSE;
BEGINBEGININSERTINTO io.thecodeforge.test.test_orders (
order_id, customer_id, order_date
) VALUES (
999999,
-999999, -- customer_id that definitely does not existSYSDATE
);
EXCEPTIONWHEN io.thecodeforge.error_pkg.e_fk_parent_not_found THEN
v_caught := TRUE;
WHENOTHERSTHENfail('test_fk_parent_not_found_mapping',
'WHEN OTHERS caught instead of e_fk_parent_not_found. SQLCODE: ' || SQLCODE);
END;
ROLLBACK;
IFNOT v_caught THENfail('test_fk_parent_not_found_mapping',
'e_fk_parent_not_found did not fire for ORA-02291');
ENDIF;
pass('test_fk_parent_not_found_mapping');
END test_fk_parent_not_found_mapping;
-- ---------------------------------------------------- Test: e_no_data_found catches ORA-01403-- --------------------------------------------------PROCEDURE test_no_data_found_mapping IS
v_caught BOOLEAN := FALSE;
v_dummy VARCHAR2(100);
BEGINBEGINSELECT customer_name INTO v_dummy
FROM io.thecodeforge.customers
WHERE customer_id = -999999; -- guaranteed to not existEXCEPTIONWHEN io.thecodeforge.error_pkg.e_no_data_found THEN
v_caught := TRUE;
WHENOTHERSTHENfail('test_no_data_found_mapping',
'WHEN OTHERS caught instead of e_no_data_found. SQLCODE: ' || SQLCODE);
END;
IFNOT v_caught THENfail('test_no_data_found_mapping',
'e_no_data_found did not fire for ORA-01403');
ENDIF;
pass('test_no_data_found_mapping');
END test_no_data_found_mapping;
-- ---------------------------------------------------- Test: custom error range mapping (-20001)-- --------------------------------------------------PROCEDURE test_custom_error_range_mapping IS
v_caught BOOLEAN := FALSE;
BEGINBEGIN
RAISE_APPLICATION_ERROR(-20001, 'Test business rule violation');
EXCEPTIONWHEN io.thecodeforge.error_pkg.e_business_rule_violation THEN
v_caught := TRUE;
WHENOTHERSTHENfail('test_custom_error_range_mapping',
'WHEN OTHERS caught instead of e_business_rule_violation. SQLCODE: ' || SQLCODE);
END;
IFNOT v_caught THENfail('test_custom_error_range_mapping',
'e_business_rule_violation did not fire for ORA-20001');
ENDIF;
pass('test_custom_error_range_mapping');
END test_custom_error_range_mapping;
-- ---------------------------------------------------- Test: verify declaration exists (for errors we cannot-- safely raise artificially in production-adjacent schemas)-- --------------------------------------------------PROCEDURE test_snapshot_too_old_is_declared IS
v_exists NUMBER;
BEGINSELECTCOUNT(*) INTO v_exists
FROM dba_source
WHERE owner = 'IO'AND name = 'ERROR_PKG'ANDUPPER(text) LIKE'%E_SNAPSHOT_TOO_OLD%'ANDUPPER(text) LIKE'%PRAGMA%';
IF v_exists = 0THENfail('test_snapshot_too_old_is_declared',
'e_snapshot_too_old not found in error_pkg — add PRAGMA EXCEPTION_INIT(e_snapshot_too_old, -1555)');
ENDIF;
pass('test_snapshot_too_old_is_declared');
END test_snapshot_too_old_is_declared;
-- ---------------------------------------------------- Run all tests and report results-- --------------------------------------------------PROCEDURE run_all_tests IS
v_start TIMESTAMP := SYSTIMESTAMP;
BEGIN
DBMS_OUTPUT.PUT_LINE(
'=== io.thecodeforge.error_pkg test suite ===' || CHR(10) ||
'Started: ' || TO_CHAR(v_start, 'YYYY-MM-DD HH24:MI:SS')
);
test_unique_violation_mapping;
test_fk_parent_not_found_mapping;
test_no_data_found_mapping;
test_custom_error_range_mapping;
test_snapshot_too_old_is_declared;
DBMS_OUTPUT.PUT_LINE(
CHR(10) || 'All tests passed. Duration: ' ||
EXTRACT(SECONDFROM (SYSTIMESTAMP - v_start)) || 's'
);
EXCEPTIONWHENOTHERSTHEN
DBMS_OUTPUT.PUT_LINE('TEST SUITE FAILED: ' || SQLERRM);
RAISE; -- Fail the CI/CD stepEND run_all_tests;
END io.thecodeforge.test.error_pkg_test;
/
-- Execute tests (add this to your CI/CD pipeline step)BEGIN
io.thecodeforge.test.error_pkg_test.run_all_tests;
END;
/
Pragma Test Strategy — What to Test and How
Test every exception that deliberately catches and recovers — if your code relies on the named exception firing, that is the one that must be tested
For errors that are dangerous to raise artificially (ORA-01555, ORA-04030, ORA-00060), use the declaration-existence test pattern — query DBA_SOURCE to verify the pragma is present
Test custom error range exceptions (-20001 to -20999) using RAISE_APPLICATION_ERROR in the test setup — these are the easiest to test and often the most overlooked
Run the test suite after every Oracle patch: minor patch sets occasionally change error message formats, which can break REGEXP_SUBSTR-based constraint name extraction even when the error number stays the same
Include the test suite in your deployment pipeline as a post-deploy smoke test — if error_pkg deployed correctly, the tests pass; if not, the tests fail before dependent packages go into service
Production Insight
Untested pragma declarations are technical debt that only manifests during incidents — which is the worst possible time to discover a binding is broken.
Build the test suite as part of the error_pkg development, not as an afterthought. The test for e_unique_violation takes 10 minutes to write and prevents hours of incident confusion.
In Oracle 19c+ environments with utPLSQL or Oracle Database Unit Testing, integrate pragma tests into the standard unit test framework so they run in CI/CD automatically on every package change.
Post every Oracle quarterly patch application, run the full pragma test suite before declaring the environment stable for production traffic.
Key Takeaway
Every PRAGMA EXCEPTION_INIT needs a test that deliberately raises the bound error and confirms the named exception fires — not WHEN OTHERS.
Tests for dangerous-to-raise errors (ORA-01555, ORA-04030) verify declaration existence via DBA_SOURCE.
Run the test suite after every Oracle patch, schema DDL change, and error_pkg deployment.
PRAGMA EXCEPTION_INIT for Custom Application Error Ranges
The -20000 to -20999 error range is reserved for application-defined errors raised with RAISE_APPLICATION_ERROR. Most teams use this range for business rule violations but leave the error numbers undocumented and unnamed, which means every caller must parse SQLERRM to understand what happened. Applying PRAGMA EXCEPTION_INIT to custom error ranges treats them as first-class named exceptions — exactly like built-in Oracle errors.
The key discipline is range allocation. Without it, different teams use the same -20001 for different business conditions, and the named exception in error_pkg catches the wrong business error in the wrong package. Establish a range allocation table in your architecture documentation and enforce it during code review.
A secondary benefit: when a custom error propagates through multiple call layers, the ORA-06512 stack shows the call chain but the error message shows your custom text. Named exceptions for custom errors make the WHEN clause readable without changing the message content.
custom_error_ranges.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
-- ===================================================-- Custom error range allocation-- Document this in your architecture wiki-- ===================================================-- -20001 to -20099: Business rule violations-- -20001: Generic business rule-- -20002: Order state machine violation-- -20003: Payment limit exceeded-- -20004: Customer account suspended---- -20100 to -20199: Data quality errors-- -20101: Invalid data format-- -20102: Missing required field-- -20103: Reference data not found---- -20200 to -20299: Configuration errors-- -20201: Missing configuration key-- -20202: Invalid configuration value---- -20300 to -20399: Integration errors-- -20301: Upstream system unavailable-- -20302: Response format unexpected-- -20303: Idempotency key conflict-- ===================================================-- Package specification: full custom error range declarationsCREATEORREPLACEPACKAGE io.thecodeforge.error_pkg AS-- Business rule violations
e_business_rule EXCEPTION;
PRAGMA EXCEPTION_INIT(e_business_rule, -20001);
e_order_state_violation EXCEPTION;
PRAGMA EXCEPTION_INIT(e_order_state_violation, -20002);
e_payment_limit_exceeded EXCEPTION;
PRAGMA EXCEPTION_INIT(e_payment_limit_exceeded, -20003);
e_account_suspended EXCEPTION;
PRAGMA EXCEPTION_INIT(e_account_suspended, -20004);
-- Data quality errors
e_invalid_data_format EXCEPTION;
PRAGMA EXCEPTION_INIT(e_invalid_data_format, -20101);
e_missing_required_field EXCEPTION;
PRAGMA EXCEPTION_INIT(e_missing_required_field, -20102);
e_reference_not_found EXCEPTION;
PRAGMA EXCEPTION_INIT(e_reference_not_found, -20103);
-- Integration errors
e_integration_unavailable EXCEPTION;
PRAGMA EXCEPTION_INIT(e_integration_unavailable, -20301);
e_idempotency_conflict EXCEPTION;
PRAGMA EXCEPTION_INIT(e_idempotency_conflict, -20303);
END io.thecodeforge.error_pkg;
/
-- ===================================================-- Usage: raising and catching custom errors-- The caller catches by name, not by error number-- ===================================================-- Payment validation procedure: raises named custom errorsCREATEORREPLACEPROCEDURE io.thecodeforge.payment.validate_payment(
p_customer_id INNUMBER,
p_amount INNUMBER
) IS
v_limit NUMBER;
v_status VARCHAR2(20);
BEGINSELECT credit_limit, account_status
INTO v_limit, v_status
FROM io.thecodeforge.customers
WHERE customer_id = p_customer_id;
IF v_status = 'SUSPENDED'THEN
RAISE_APPLICATION_ERROR(-20004,
'Account ' || p_customer_id || ' is suspended — payment rejected');
ENDIF;
IF p_amount > v_limit THEN
RAISE_APPLICATION_ERROR(-20003,
'Payment amount ' || p_amount ||
' exceeds credit limit ' || v_limit ||
' for account ' || p_customer_id);
ENDIF;
EXCEPTIONWHEN io.thecodeforge.error_pkg.e_no_data_found THEN
RAISE_APPLICATION_ERROR(-20103,
'Customer ' || p_customer_id || ' not found in reference data');
END validate_payment;
/
-- Order creation procedure: catches named custom errors from validate_paymentCREATEORREPLACEPROCEDURE io.thecodeforge.order.create_order_with_payment(
p_customer_id INNUMBER,
p_amount INNUMBER
) ISBEGIN
io.thecodeforge.payment.validate_payment(p_customer_id, p_amount);
INSERTINTO io.thecodeforge.orders (
customer_id, order_amount, status, created_at
) VALUES (
p_customer_id, p_amount, 'PENDING', SYSTIMESTAMP
);
COMMIT;
EXCEPTIONWHEN io.thecodeforge.error_pkg.e_account_suspended THEN-- Readable: account suspended — specific business response
io.thecodeforge.logging.log_warning(
'ORDER_REJECTED',
'Account suspended: ' || p_customer_id
);
-- Return a specific response code to the caller instead of raisingROLLBACK;
WHEN io.thecodeforge.error_pkg.e_payment_limit_exceeded THEN-- Readable: payment limit — offer partial order option
io.thecodeforge.logging.log_info(
'ORDER_LIMIT',
'Payment limit exceeded for customer: ' || p_customer_id ||
' Amount: ' || p_amount
);
ROLLBACK;
RAISE_APPLICATION_ERROR(-20003,
'Order amount exceeds your credit limit. Contact support to increase limit.');
WHEN io.thecodeforge.error_pkg.e_reference_not_found THEN-- Customer not found in validate_payment — data quality issue
io.thecodeforge.logging.log_error(
'ORDER_DATA_QUALITY',
'Customer ' || p_customer_id || ' referenced in order but not in customers table'
);
ROLLBACK;
RAISE;
END create_order_with_payment;
/
Custom Error Range — Critical Rules
Document your range allocation in writing before writing a single RAISE_APPLICATION_ERROR call — undocumented ranges collide within months in active teams
Each -200XX error number should have exactly one meaning across the entire codebase — a named exception bound to it in error_pkg makes this enforceable via code review
Never use -20000 — Oracle reserves it; use -20001 as your first custom error
The custom error message from RAISE_APPLICATION_ERROR appears in SQLERRM and in the ORA-06512 stack — make it human-readable: include the business entity, the violating value, and what action to take
In REST API layers or microservice boundaries, map each custom error number to a specific HTTP status code and response body — this mapping should also be documented with the range allocation table
Production Insight
Custom error ranges are architectural contracts, not implementation details.
Any team that calls your PL/SQL packages needs to know which error numbers to expect and what they mean.
Publish your error range allocation as a table in your team wiki and link to it from the error_pkg header comment.
In Oracle 19c+ with Application Containers, consider using application-level error packages that are versioned alongside your schema — this keeps error number allocations synchronized with schema versions.
Key Takeaway
Apply PRAGMA EXCEPTION_INIT to custom error ranges exactly as you do for Oracle built-in errors.
Allocate error number ranges by subsystem and document the allocation before coding.
Custom error messages should be human-readable: entity, value, and recommended action.
Treat error number allocations as architectural contracts — breaking them requires a coordinated change across all callers.
● Production incidentPOST-MORTEMseverity: high
Undiagnosable Constraint Violation in Payment Processing
Symptom
Batch job logged ORA-00001 followed by ORA-06512 pointing to io.thecodeforge.payment.reconcile_batch at line 247. The exception handler caught SQLCODE = -1 and logged a generic 'unique constraint violated' message with no constraint name, no table name, and no offending key values. The batch continued processing, silently skipping the failed inserts.
Assumption
The developer used a single WHEN SQLCODE = -1 handler assuming all unique constraint violations in the reconciliation batch require the same response: log and skip. They reasoned that any duplicate in the reconciliation feed was a benign retry from the upstream payment processor.
Root cause
The payment system has 14 unique constraints across 6 tables. The generic SQLCODE handler suppressed the constraint name from SQLERRM by logging only the SQLCODE and a static message. Engineers had to correlate error timestamps with AWR ASH data and then manually test each of the 14 constraints to find which one was firing. The actual issue was a race condition on io.thecodeforge.payment.transaction_log unique index on (batch_id, sequence_number) — caused by a bug in the upstream processor that re-sent the same sequence numbers with corrected amounts, which were not duplicates from a business perspective but triggered the constraint. The batch skipped 1,847 corrected payment records over 4 hours.
Fix
1. Declared separate PRAGMA EXCEPTION_INIT for each of the 14 constraints — each bound to -1 with a constraint-specific name (e_txn_batch_seq_unique, e_customer_email_unique, etc.)
2. Added REGEXP_SUBSTR extraction of the constraint name from SQLERRM as a fallback inside the WHEN e_unique_violation handler for any constraint not yet named
3. Created io.thecodeforge.error_pkg with pre-declared exceptions for all production constraints, generated from USER_CONSTRAINTS query output
4. Replaced all generic SQLCODE = -1 handlers in the 6 payment packages with named exceptions
5. Added a test procedure that deliberately violates each constraint and verifies the correct named exception fires
6. Fixed the upstream race condition with an idempotent MERGE that accepts corrected amounts for existing (batch_id, sequence_number) pairs
Key lesson
Never use generic SQLCODE = -1 when multiple constraints can fail — the constraint name is the diagnosis
PRAGMA EXCEPTION_INIT with constraint-specific names enables instant identification without SQLERRM parsing during incidents
Centralize exception declarations in a shared package so the whole team uses consistent names
Log and continue is only safe if you log enough context to reconstruct what was skipped — constraint name, offending key values, and record identifier at minimum
Generate exception declarations from USER_CONSTRAINTS — manual enumeration drifts from the actual schema within weeks
Production debug guideSymptom-to-action mapping for pragma declaration, compilation, and runtime behavior issues6 entries
Symptom · 01
PLS-00702: pragma must be declared in declarative part
→
Fix
Move the PRAGMA EXCEPTION_INIT statement before the BEGIN keyword. It must appear after the exception name declaration but before any executable statements. The declaration order matters: exception name first, then pragma. Check for nested BEGIN/END blocks where the pragma was accidentally placed inside the executable section.
Symptom · 02
Named exception compiles without error but never fires — errors fall through to WHEN OTHERS
→
Fix
Verify two things: (1) the error number in the pragma is negative — PRAGMA EXCEPTION_INIT(e_name, 1403) silently fails, it must be -1403; (2) the error number matches exactly — Oracle error numbers do not have leading zeros in the pragma even though ORA-01403 has a leading zero in the display name. Check with: SELECT text FROM dba_source WHERE name = 'YOUR_PACKAGE' AND UPPER(text) LIKE '%PRAGMA%'.
Symptom · 03
Named exception declared in package body is not visible in other packages
→
Fix
Move both the EXCEPTION declaration and the PRAGMA EXCEPTION_INIT to the package specification, not the body. Only declarations in the specification are visible to external callers. Reference using package_name.exception_name syntax. If the package was not recompiled after moving to the spec, force recompilation with ALTER PACKAGE owner.package_name COMPILE SPECIFICATION.
Symptom · 04
Exception fires but SQLERRM returns ORA-06510: PL/SQL: unhandled user-defined exception instead of the original Oracle error
→
Fix
This happens when you RAISE a user-defined exception that was bound with PRAGMA EXCEPTION_INIT — the original Oracle error context is replaced. Use RAISE_APPLICATION_ERROR to propagate the original error with context, or capture DBMS_UTILITY.FORMAT_ERROR_STACK before re-raising to preserve the full original stack.
Symptom · 05
PRAGMA EXCEPTION_INIT for -20001 to -20999 range does not fire for RAISE_APPLICATION_ERROR calls
→
Fix
Verify that the RAISE_APPLICATION_ERROR call uses the exact error number you bound. RAISE_APPLICATION_ERROR(-20001, 'message') fires e_business_rule_violation bound to -20001 correctly. If the call uses -20002, you need a separate exception bound to -20002. Custom error ranges require exact matches — there is no range binding in PRAGMA EXCEPTION_INIT.
Symptom · 06
Named exception fires correctly in development but falls through to WHEN OTHERS in production
→
Fix
Check whether the production and development schemas have different constraint names (this happens with schema-prefixed constraints in multi-tenant deployments). Also verify that the error_pkg was compiled on the production instance — query SELECT status FROM dba_objects WHERE object_name = 'ERROR_PKG' to confirm it is VALID, not INVALID.
★ PRAGMA EXCEPTION_INIT Quick ReferenceCopy-paste ready commands for common pragma declaration, compilation, and runtime issues in production environments
Need to find all PRAGMA EXCEPTION_INIT declarations in the codebase−
Immediate action
Query DBA_SOURCE for all pragma declarations across all schemas
Commands
SELECT owner, name, type, line, TRIM(text) AS declaration
FROM dba_source
WHERE UPPER(text) LIKE '%PRAGMA EXCEPTION_INIT%'
AND owner NOT IN ('SYS','SYSTEM','DBSNMP')
ORDER BY owner, name, line;
-- Check for invalid packages containing pragma declarations
SELECT o.owner, o.object_name, o.object_type,
o.status, o.last_ddl_time
FROM dba_objects o
WHERE o.status = 'INVALID'
AND EXISTS (
SELECT 1 FROM dba_source s
WHERE s.owner = o.owner
AND s.name = o.object_name
AND UPPER(s.text) LIKE '%PRAGMA EXCEPTION_INIT%'
)
ORDER BY o.last_ddl_time DESC;
Fix now
Create or update io.thecodeforge.error_pkg as the single centralized location for all pragma declarations. Recompile invalid packages with DBMS_UTILITY.COMPILE_SCHEMA.
Need to generate PRAGMA EXCEPTION_INIT declarations for all unique constraints+
Immediate action
Query USER_CONSTRAINTS to generate declaration code automatically
Commands
-- Generate exception declarations from data dictionary
SELECT
' e_' || LOWER(REGEXP_REPLACE(constraint_name, '[^A-Z0-9]', '_')) ||
' EXCEPTION;' || CHR(10) ||
' PRAGMA EXCEPTION_INIT(e_' ||
LOWER(REGEXP_REPLACE(constraint_name, '[^A-Z0-9]', '_')) ||
', -1); -- ' || table_name || ' unique constraint'
AS generated_declaration
FROM user_constraints
WHERE constraint_type IN ('U', 'P')
ORDER BY table_name, constraint_name;
-- Verify all generated exceptions are in error_pkg
SELECT uc.constraint_name, uc.table_name,
CASE WHEN ds.text IS NOT NULL THEN 'DECLARED'
ELSE 'MISSING' END AS status
FROM user_constraints uc
LEFT JOIN dba_source ds
ON UPPER(ds.text) LIKE '%' || LOWER(uc.constraint_name) || '%'
AND ds.name = 'ERROR_PKG'
WHERE uc.constraint_type IN ('U','P')
ORDER BY status DESC, uc.table_name;
Fix now
Copy generated declarations into io.thecodeforge.error_pkg specification and recompile. Run the test suite to verify each binding fires correctly.
Exception handler catches wrong error or named exception stops firing after DDL change+
Immediate action
Verify pragma binding and recompile error package
Commands
-- Check pragma declarations in the error package
SELECT line, TRIM(text) AS source_text
FROM dba_source
WHERE owner = 'IO'
AND name = 'ERROR_PKG'
AND (UPPER(text) LIKE '%EXCEPTION%' OR UPPER(text) LIKE '%PRAGMA%')
ORDER BY line;
-- Check for compilation errors in error package and its dependents
SELECT e.name, e.type, e.line, e.position, e.text AS error_text
FROM user_errors e
WHERE e.name IN ('ERROR_PKG', 'YOUR_PACKAGE')
ORDER BY e.name, e.sequence;
-- Recompile the package and its dependents
BEGIN
DBMS_UTILITY.COMPILE_SCHEMA(
schema => 'IO',
compile_all => FALSE, -- only invalid objects
reuse_settings => TRUE
);
END;
Fix now
After DDL changes (new constraints, dropped constraints, renamed columns), regenerate the error_pkg from USER_CONSTRAINTS and run the full pragma test suite before deploying to production.
Error Handling Approaches Comparison
Approach
Readability
Performance
Maintainability
Best For
WHEN SQLCODE = -1403
Low — requires mental translation from number to meaning
Fast — direct integer comparison at runtime
Poor — error numbers scattered across procedures; no single search reveals all usages
PRAGMA EXCEPTION_INIT binds named exceptions to Oracle error numbers at compile time
zero runtime overhead for the binding, significant readability gain in every WHEN clause
2
Centralize all declarations in io.thecodeforge.error_pkg specification
one source of truth eliminates scattered numeric SQLCODE comparisons and inconsistent exception naming
3
Map each unique constraint to its own named exception
generic SQLCODE = -1 handlers hide which constraint failed and extend incident diagnosis from minutes to hours
4
Always use negative error numbers
positive values compile without error but bind silently fails; only a unit test will catch this
5
Apply PRAGMA EXCEPTION_INIT to custom -20000 range errors exactly as you do for Oracle built-in errors
document your range allocation before writing any RAISE_APPLICATION_ERROR call
6
Test every pragma declaration by deliberately raising the bound error and asserting the named exception fires
run the test suite after Oracle patches, schema DDL changes, and error_pkg deployments
Common mistakes to avoid
6 patterns
×
Declaring PRAGMA EXCEPTION_INIT after the BEGIN keyword
Symptom
PLS-00702: pragma must be declared in declarative part. Compilation fails before deployment — which is the correct behavior. The risk is that developers fix it by moving the pragma somewhere that compiles but does not bind correctly.
Fix
Move the PRAGMA EXCEPTION_INIT line to the declarative section: after the exception name declaration and before the BEGIN keyword. The correct order in the declarative section is: variable declarations, then exception declarations, then pragma declarations. Pragmas must follow the exceptions they reference.
×
Using positive error numbers in PRAGMA EXCEPTION_INIT
Symptom
Compilation succeeds with no warnings. The named exception never fires in any test or production run. Errors fall through to WHEN OTHERS or propagate uncaught. This can go undetected for months if the code path is not exercised frequently.
Fix
Always use negative error numbers. ORA-01403 is -1403, ORA-00001 is -1. Add a unit test that deliberately raises the error and asserts the named exception fires — this is the only reliable way to catch positive-number mistakes since the compiler does not flag them.
×
Declaring PRAGMA EXCEPTION_INIT in package body instead of specification
Symptom
The package compiles successfully. Other packages that try to reference the exception get PLS-00201: identifier 'PACKAGE_NAME.EXCEPTION_NAME' must be declared. Teams work around this by redeclaring the exception in every package that needs it, creating inconsistent names for the same error condition.
Fix
Declare both the EXCEPTION and its PRAGMA EXCEPTION_INIT in the package specification. Only specification-level declarations are visible to external packages. After moving to the specification, force recompilation of all dependent packages with DBMS_UTILITY.COMPILE_SCHEMA.
×
Using generic SQLCODE = -1 for all unique constraint violations when the schema has multiple unique constraints
Symptom
Error log shows 'unique constraint violated' with no constraint name, no table name, no offending key. During incidents, engineers must correlate error timestamps with AWR data and manually test each constraint to find which one fired. This takes hours for schemas with more than 5 unique constraints.
Fix
Design procedures so each one can only violate one specific constraint — then the handler name communicates the constraint implicitly. For procedures that can violate multiple constraints, extract the constraint name from SQLERRM with REGEXP_SUBSTR and branch on it. Log the constraint name in every unique violation handler, always.
×
Not testing pragma declarations after Oracle upgrades or schema DDL changes
Symptom
Named exceptions silently stop matching the correct errors after an Oracle patch or a constraint rename. The binding compiles correctly but the exception never fires in production because the underlying behavior shifted. Errors fall through to WHEN OTHERS, which may log them differently or re-raise without context.
Fix
Maintain a test suite (as shown in the Testing section) that deliberately raises each bound error. Add this suite to your CI/CD pipeline as a post-deploy smoke test. Run it after every Oracle quarterly patch, every schema DDL deployment, and every error_pkg release.
×
Mixing custom error numbers across teams without a range allocation document
Symptom
Two different packages raise RAISE_APPLICATION_ERROR(-20001, ...) for two completely different business conditions. Callers that catch e_business_rule_violation bound to -20001 execute the wrong recovery logic for half of the errors. The bug is invisible until the wrong recovery action causes a data inconsistency.
Fix
Document your -20000 to -20999 range allocation before writing any RAISE_APPLICATION_ERROR call. Enforce it during code review. Put the allocation table in the error_pkg header comment so it is visible to anyone reading the package.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
How would you refactor a legacy PL/SQL codebase with 200+ procedures tha...
Q02SENIOR
What is the difference between PRAGMA EXCEPTION_INIT and RAISE_APPLICATI...
Q03SENIOR
How do you handle the situation where multiple unique constraints all pr...
Q04JUNIOR
What happens if you declare PRAGMA EXCEPTION_INIT with a positive error ...
Q05SENIOR
Explain when PRAGMA EXCEPTION_INIT is insufficient and what you use inst...
Q01 of 05SENIOR
How would you refactor a legacy PL/SQL codebase with 200+ procedures that use WHEN SQLCODE = comparisons throughout to use PRAGMA EXCEPTION_INIT instead?
ANSWER
Start with an audit: query DBA_SOURCE for all WHEN SQLCODE occurrences and catalog every unique error number in use. Group them by frequency and severity — fix the highest-volume errors first because that is where you get the most diagnostic value. Create io.thecodeforge.error_pkg with named exceptions and PRAGMA EXCEPTION_INIT for each unique error number. For constraint-specific handling, query USER_CONSTRAINTS and generate declarations. Then refactor incrementally, one package at a time, replacing SQLCODE comparisons with named exception references. Add a unit test per named exception before refactoring the first handler that uses it — this ensures you do not break existing catch behavior. Deploy in stages, not as a big-bang release, so you can isolate regressions. Wrap up by adding the pragma test suite to your CI/CD pipeline so future engineers cannot accidentally break bindings.
Q02 of 05SENIOR
What is the difference between PRAGMA EXCEPTION_INIT and RAISE_APPLICATION_ERROR, and when would you use each?
ANSWER
They solve different problems and work together, not in competition. PRAGMA EXCEPTION_INIT is a compile-time directive that gives a readable name to an existing Oracle error number — built-in errors like ORA-01403, ORA-00001, or even custom -20001 errors. It makes the WHEN clause self-documenting. RAISE_APPLICATION_ERROR is a runtime function that creates new error conditions in the -20000 to -20999 range with custom messages — it defines what new errors mean for your application. The production pattern is: use RAISE_APPLICATION_ERROR to raise business-specific errors at enforcement points, then use PRAGMA EXCEPTION_INIT to give those same error numbers readable names in every package that catches them. One defines the error; the other names it for the catcher.
Q03 of 05SENIOR
How do you handle the situation where multiple unique constraints all produce ORA-00001 and you need to distinguish them in a single exception handler?
ANSWER
You cannot have Oracle dispatch selectively to different named exceptions in the same EXCEPTION block when they are all bound to the same error number — the first matching WHEN clause wins, regardless of which constraint fired. The solutions are: (1) design procedures so each one can only violate one specific constraint — then the handler is unambiguous by construction; (2) for procedures that unavoidably touch multiple constraints, catch the generic e_unique_violation and use REGEXP_SUBSTR on SQLERRM to extract the constraint name, then branch with IF/ELSIF; (3) wrap each INSERT in its own sub-block with its own handler, so each sub-block's context makes the constraint implicit. The constraint name extraction approach is the fallback, not the primary design. If you find yourself writing a 10-way IF/ELSIF on constraint names, the procedure is doing too many things and should be decomposed.
Q04 of 05JUNIOR
What happens if you declare PRAGMA EXCEPTION_INIT with a positive error number instead of negative, and how would you detect this in production?
ANSWER
The declaration compiles without error or warning — the compiler accepts it. The binding silently fails. The named exception will never fire because Oracle error codes are negative integers by convention, and the compiler does not validate that the error number you provided is a real Oracle error. In production, errors that should be caught by the named exception fall through to WHEN OTHERS or propagate uncaught. Detection requires a unit test that deliberately raises the bound error and asserts the named exception fired. Without that test, you will not know until an incident reveals that your handler did not execute. Always follow every pragma declaration with a test — compilation success is not evidence that the binding works.
Q05 of 05SENIOR
Explain when PRAGMA EXCEPTION_INIT is insufficient and what you use instead.
ANSWER
Two scenarios. First: dynamic SQL built at runtime, where you cannot know at compile time which tables or constraints the generated statement will touch. PRAGMA EXCEPTION_INIT requires compile-time knowledge of the error. For dynamic SQL, catch the generic named exception (e_unique_violation) and extract the constraint name from SQLERRM or DBMS_UTILITY.FORMAT_ERROR_STACK using REGEXP_SUBSTR at runtime. Always log the SQL string alongside the error. Second: cross-environment deployments where constraint names differ between development, staging, and production. Named exceptions bound to specific constraint names (which all share -1 anyway) do not help here — you need runtime constraint name extraction from SQLERRM. The correct architecture uses both: PRAGMA EXCEPTION_INIT for the error category (unique violation, FK violation, etc.) and SQLERRM parsing for the specific constraint within that category.
01
How would you refactor a legacy PL/SQL codebase with 200+ procedures that use WHEN SQLCODE = comparisons throughout to use PRAGMA EXCEPTION_INIT instead?
SENIOR
02
What is the difference between PRAGMA EXCEPTION_INIT and RAISE_APPLICATION_ERROR, and when would you use each?
SENIOR
03
How do you handle the situation where multiple unique constraints all produce ORA-00001 and you need to distinguish them in a single exception handler?
SENIOR
04
What happens if you declare PRAGMA EXCEPTION_INIT with a positive error number instead of negative, and how would you detect this in production?
JUNIOR
05
Explain when PRAGMA EXCEPTION_INIT is insufficient and what you use instead.
SENIOR
FAQ · 6 QUESTIONS
Frequently Asked Questions
01
Can one exception name be bound to multiple error numbers using multiple PRAGMA EXCEPTION_INIT statements?
No. Each exception name can be bound to exactly one error number. The second PRAGMA EXCEPTION_INIT for the same exception name overrides the first without a compilation error, which means you end up with the last binding winning and the first one silently lost. If you need to handle multiple error numbers, declare separate exception names for each. In a single EXCEPTION block, Oracle fires the first matching WHEN clause — so you can have multiple named exceptions each bound to different error numbers, and Oracle routes to the correct one.
Was this helpful?
02
Does PRAGMA EXCEPTION_INIT work with custom error numbers created by RAISE_APPLICATION_ERROR?
Yes, and this is one of the most underused applications of the pragma. You can bind a named exception to any error number in the -20000 to -20999 range. This means callers can write WHEN io.thecodeforge.error_pkg.e_account_suspended instead of WHEN SQLCODE = -20004, which communicates the business condition rather than an arbitrary number. The pattern works across package boundaries: the raising package uses RAISE_APPLICATION_ERROR(-20004, message), and any catching package uses the named exception bound in error_pkg.
Was this helpful?
03
What is the performance difference between WHEN SQLCODE = -1403 and using PRAGMA EXCEPTION_INIT, and does it matter in practice?
Named exceptions with PRAGMA EXCEPTION_INIT are slightly faster because the error-to-handler mapping is compiled into the object at compilation time — no integer comparison at runtime. The dispatch is essentially a lookup in a pre-built table. The SQLCODE = -1403 pattern evaluates an integer comparison for each WHEN clause tested. In procedures that raise exceptions millions of times (high-volume batch processing with expected NO_DATA_FOUND patterns), the difference is measurable — roughly 10–15% faster exception dispatch. For most code paths where exceptions are genuinely exceptional, the difference is negligible. The readability and maintainability benefit is significant in any case and is the primary reason to use PRAGMA EXCEPTION_INIT, not the performance delta.
Was this helpful?
04
How do I find all PRAGMA EXCEPTION_INIT declarations in my codebase?
Query DBA_SOURCE: SELECT owner, name, type, line, TRIM(text) FROM dba_source WHERE UPPER(text) LIKE '%PRAGMA EXCEPTION_INIT%' AND owner NOT IN ('SYS','SYSTEM') ORDER BY owner, name, line. This returns every declaration with its file position. For a centralized package, query by name directly. To also find scattered declarations outside the central package (which you may want to consolidate), extend the query to exclude your error_pkg name and review the results — these are candidates for migration to the centralized package.
Was this helpful?
05
Can PRAGMA EXCEPTION_INIT be used in anonymous PL/SQL blocks?
Yes. PRAGMA EXCEPTION_INIT works in any PL/SQL block that has a declarative section — anonymous blocks, named procedures, functions, triggers, and package bodies all support it. The declaration must appear in the declarative section before the BEGIN keyword. For anonymous blocks used in scripts and ad-hoc debugging, local pragma declarations are fine. For any code that runs in production (whether as a stored procedure or as a scheduled script), prefer referencing exceptions from the centralized error_pkg rather than re-declaring locally — this keeps naming consistent and avoids the drift problem.
Was this helpful?
06
What happens to an ORA-06512 stack when a PRAGMA EXCEPTION_INIT exception is re-raised?
When you catch a named exception and re-raise it with RAISE (not RAISE_APPLICATION_ERROR), the original Oracle error and its full ORA-06512 stack are preserved. The caller sees the same error number and the same stack trace as if the exception had not been caught at the intermediate layer. If you use RAISE_APPLICATION_ERROR instead of RAISE, the original error number is replaced with your custom -20000 range number and the original ORA-06512 stack may be lost — which is why you should capture DBMS_UTILITY.FORMAT_ERROR_STACK before calling RAISE_APPLICATION_ERROR when you need to preserve the original context for logging.