Senior 6 min · April 14, 2026

PRAGMA EXCEPTION_INIT — Name Constraints Avoid Silent Skips

A batch skipped 1,847 payment records because generic ORA-00001 hid the constraint name.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • 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 usage
DECLARE
  e_no_customer    EXCEPTION;
  PRAGMA EXCEPTION_INIT(e_no_customer, -1403);  -- ORA-01403: no data found
  v_customer_name  VARCHAR2(200);
BEGIN
  SELECT customer_name INTO v_customer_name
  FROM io.thecodeforge.customers
  WHERE customer_id = p_customer_id;
  
  RETURN v_customer_name;
EXCEPTION
  WHEN 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'
    );
    RETURN NULL;
END;
/

-- Pattern 2: Multiple exceptions in the same declarative section
DECLARE
  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: deadlock
BEGIN
  INSERT INTO io.thecodeforge.orders (
    order_id, customer_id, order_date
  ) VALUES (
    p_order_id, p_customer_id, SYSDATE
  );
EXCEPTION
  WHEN 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 retry
    ROLLBACK;
    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 time
DECLARE
  e_my_exception EXCEPTION;
BEGIN
  PRAGMA EXCEPTION_INIT(e_my_exception, -1403);  -- COMPILATION ERROR
  SELECT customer_name INTO v_name FROM customers WHERE customer_id = p_id;
END;
/

-- WRONG 2: Positive error number — silent failure, exception never fires
DECLARE
  e_my_exception EXCEPTION;
  PRAGMA EXCEPTION_INIT(e_my_exception, 1403);   -- WRONG: must be -1403
BEGIN
  NULL;
EXCEPTION
  WHEN e_my_exception THEN
    NULL;  -- This handler will NEVER execute
END;
/

-- WRONG 3: Reversed argument order — compiles but binding is incorrect
DECLARE
  e_my_exception EXCEPTION;
  PRAGMA EXCEPTION_INIT(-1403, e_my_exception);  -- WRONG argument order
BEGIN
  NULL;
END;
/

-- WRONG 4: Forward reference — exception not yet declared
DECLARE
  PRAGMA EXCEPTION_INIT(e_my_exception, -1403);  -- WRONG: declared before exception
  e_my_exception EXCEPTION;
BEGIN
  NULL;
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
-- ===================================================
CREATE OR REPLACE PACKAGE io.thecodeforge.error_pkg AS

  -- --------------------------------------------------
  -- DATA INTEGRITY EXCEPTIONS
  -- --------------------------------------------------
  e_unique_violation      EXCEPTION;           -- ORA-00001
  PRAGMA EXCEPTION_INIT(e_unique_violation, -1);
  
  e_fk_parent_not_found   EXCEPTION;           -- ORA-02291
  PRAGMA EXCEPTION_INIT(e_fk_parent_not_found, -2291);
  
  e_fk_child_exists       EXCEPTION;           -- ORA-02292
  PRAGMA EXCEPTION_INIT(e_fk_child_exists, -2292);
  
  e_not_null_violation    EXCEPTION;           -- ORA-01400
  PRAGMA EXCEPTION_INIT(e_not_null_violation, -1400);
  
  e_value_too_large       EXCEPTION;           -- ORA-12899
  PRAGMA EXCEPTION_INIT(e_value_too_large, -12899);
  
  e_check_constraint      EXCEPTION;           -- ORA-02290
  PRAGMA EXCEPTION_INIT(e_check_constraint, -2290);

  -- --------------------------------------------------
  -- QUERY AND CURSOR EXCEPTIONS
  -- --------------------------------------------------
  e_no_data_found         EXCEPTION;           -- ORA-01403
  PRAGMA EXCEPTION_INIT(e_no_data_found, -1403);
  
  e_too_many_rows         EXCEPTION;           -- ORA-01422
  PRAGMA EXCEPTION_INIT(e_too_many_rows, -1422);
  
  e_cursor_already_open   EXCEPTION;           -- ORA-06511
  PRAGMA EXCEPTION_INIT(e_cursor_already_open, -6511);
  
  e_max_open_cursors      EXCEPTION;           -- ORA-01000
  PRAGMA EXCEPTION_INIT(e_max_open_cursors, -1000);

  -- --------------------------------------------------
  -- TYPE CONVERSION EXCEPTIONS
  -- --------------------------------------------------
  e_invalid_number        EXCEPTION;           -- ORA-01722
  PRAGMA EXCEPTION_INIT(e_invalid_number, -1722);
  
  e_value_error           EXCEPTION;           -- ORA-06502
  PRAGMA EXCEPTION_INIT(e_value_error, -6502);
  
  e_date_format_mismatch  EXCEPTION;           -- ORA-01830
  PRAGMA EXCEPTION_INIT(e_date_format_mismatch, -1830);
  
  e_invalid_month         EXCEPTION;           -- ORA-01843
  PRAGMA EXCEPTION_INIT(e_invalid_month, -1843);

  -- --------------------------------------------------
  -- CONCURRENCY AND LOCKING EXCEPTIONS
  -- --------------------------------------------------
  e_deadlock              EXCEPTION;           -- ORA-00060
  PRAGMA EXCEPTION_INIT(e_deadlock, -60);
  
  e_resource_busy         EXCEPTION;           -- ORA-00054
  PRAGMA EXCEPTION_INIT(e_resource_busy, -54);
  
  e_snapshot_too_old      EXCEPTION;           -- ORA-01555
  PRAGMA EXCEPTION_INIT(e_snapshot_too_old, -1555);

  -- --------------------------------------------------
  -- MEMORY AND RESOURCE EXCEPTIONS
  -- --------------------------------------------------
  e_out_of_process_memory EXCEPTION;           -- ORA-04030
  PRAGMA EXCEPTION_INIT(e_out_of_process_memory, -4030);
  
  e_shared_pool_exhausted EXCEPTION;           -- ORA-04031
  PRAGMA EXCEPTION_INIT(e_shared_pool_exhausted, -4031);
  
  e_package_state_discarded EXCEPTION;         -- ORA-04068
  PRAGMA EXCEPTION_INIT(e_package_state_discarded, -4068);

  -- --------------------------------------------------
  -- COLLECTION EXCEPTIONS
  -- --------------------------------------------------
  e_collection_uninit     EXCEPTION;           -- ORA-06531
  PRAGMA EXCEPTION_INIT(e_collection_uninit, -6531);
  
  e_subscript_beyond_count EXCEPTION;          -- ORA-06533
  PRAGMA 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-20001
  PRAGMA EXCEPTION_INIT(e_business_rule_violation, -20001);
  
  e_data_quality_error    EXCEPTION;           -- ORA-20101
  PRAGMA EXCEPTION_INIT(e_data_quality_error, -20101);
  
  e_configuration_error   EXCEPTION;           -- ORA-20201
  PRAGMA EXCEPTION_INIT(e_configuration_error, -20201);
  
  e_integration_error     EXCEPTION;           -- ORA-20301
  PRAGMA EXCEPTION_INIT(e_integration_error, -20301);

END io.thecodeforge.error_pkg;
/

-- ===================================================
-- Usage example: referencing error_pkg from another package
-- ===================================================
CREATE OR REPLACE PROCEDURE io.thecodeforge.order.create_order(
  p_customer_id IN NUMBER,
  p_order_id    OUT NUMBER
) IS
BEGIN
  INSERT INTO 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;
EXCEPTION
  WHEN io.thecodeforge.error_pkg.e_fk_parent_not_found THEN
    -- Instantly readable: the customer_id FK reference is invalid
    ROLLBACK;
    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 SQLERRM
    ROLLBACK;
    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 duplicates
    
  WHEN io.thecodeforge.error_pkg.e_deadlock THEN
    -- Deadlock: always roll back and signal retry-eligible
    ROLLBACK;
    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'
  END AS 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 = 'USER NAME'   -- exclude system-generated names
ORDER BY table_name, constraint_type, constraint_name;

-- ===================================================
-- Step 2: Constraint-specific exceptions in error_pkg
-- Example: payment system schema
-- ===================================================
CREATE OR REPLACE PACKAGE 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 FK
  PRAGMA EXCEPTION_INIT(e_txn_order_fk, -2291);
  
  e_order_customer_fk        EXCEPTION;        -- order -> customer FK
  PRAGMA 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
-- ===================================================
CREATE OR REPLACE PROCEDURE io.thecodeforge.payment.insert_transaction(
  p_batch_id    IN NUMBER,
  p_seq_number  IN NUMBER,
  p_order_id    IN NUMBER,
  p_amount      IN NUMBER
) IS
  v_constraint_name VARCHAR2(128);
BEGIN
  INSERT INTO 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
  );
EXCEPTION
  WHEN 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 duplicates
    ELSE
      -- 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;
    END IF;
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
)
GROUP BY constraint_name_extracted
ORDER BY 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
-- ===================================================
CREATE OR REPLACE PROCEDURE io.thecodeforge.etl.load_staging_data(
  p_source_table IN VARCHAR2,   -- dynamic: table name varies by feed
  p_target_table IN VARCHAR2
) 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 semantic
BEGIN
  -- Validate that both tables exist before building dynamic SQL
  DECLARE
    v_exists NUMBER;
  BEGIN
    SELECT COUNT(*) INTO v_exists
    FROM user_tables
    WHERE table_name = UPPER(p_source_table);
    
    IF v_exists = 0 THEN
      RAISE_APPLICATION_ERROR(-20301,
        'Source table does not exist: ' || p_source_table);
    END IF;
  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''';
  
  BEGIN
    EXECUTE IMMEDIATE v_sql;
    v_row_count := SQL%ROWCOUNT;
  EXCEPTION
    WHEN 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 continue
      
    WHEN 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 continue
      
    WHEN OTHERS THEN
      -- 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
-- ===================================================
CREATE OR REPLACE PROCEDURE io.thecodeforge.etl.smart_error_router IS
  v_error_code   NUMBER;
  v_error_msg    VARCHAR2(4000);
  v_constraint   VARCHAR2(128);
BEGIN
  -- ... DML operations ...
  NULL;
EXCEPTION
  WHEN OTHERS THEN
    v_error_code := SQLCODE;
    v_error_msg  := DBMS_UTILITY.FORMAT_ERROR_STACK;
    
    CASE v_error_code
      WHEN -1 THEN  -- 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 -2291 THEN  -- 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
        );
    END CASE;
    
    RAISE;  -- Always re-raise after logging
END 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
-- ===================================================
CREATE OR REPLACE PACKAGE io.thecodeforge.test.error_pkg_test AS
  PROCEDURE 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 declaration
  PROCEDURE test_deadlock_is_declared;
  PROCEDURE test_custom_error_range_mapping;
  PROCEDURE run_all_tests;
END io.thecodeforge.test.error_pkg_test;
/

CREATE OR REPLACE PACKAGE BODY io.thecodeforge.test.error_pkg_test AS

  -- Internal helper: fail with a clear message
  PROCEDURE fail(p_test_name IN VARCHAR2, p_message IN VARCHAR2) IS
  BEGIN
    RAISE_APPLICATION_ERROR(-20999,
      'PRAGMA TEST FAIL: ' || p_test_name || ' — ' || p_message);
  END fail;
  
  PROCEDURE pass(p_test_name IN VARCHAR2) IS
  BEGIN
    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 exists
    BEGIN
      INSERT INTO io.thecodeforge.test.test_customers (
        customer_id, email
      ) VALUES (999999, 'pragma_test@thecodeforge.io');
    EXCEPTION
      WHEN io.thecodeforge.error_pkg.e_unique_violation THEN
        NULL;  -- Already exists from a previous run — that is fine
    END;
    
    -- Now deliberately trigger ORA-00001
    BEGIN
      INSERT INTO io.thecodeforge.test.test_customers (
        customer_id, email
      ) VALUES (999999, 'pragma_test_dup@thecodeforge.io');  -- duplicate PK
    EXCEPTION
      WHEN io.thecodeforge.error_pkg.e_unique_violation THEN
        v_caught := TRUE;
      WHEN OTHERS THEN
        fail('test_unique_violation_mapping',
             'WHEN OTHERS caught instead of e_unique_violation. SQLCODE: ' || SQLCODE);
    END;
    
    ROLLBACK;
    
    IF NOT v_caught THEN
      fail('test_unique_violation_mapping',
           'e_unique_violation did not fire for ORA-00001 — check pragma binding');
    END IF;
    
    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;
  BEGIN
    BEGIN
      INSERT INTO io.thecodeforge.test.test_orders (
        order_id, customer_id, order_date
      ) VALUES (
        999999,
        -999999,  -- customer_id that definitely does not exist
        SYSDATE
      );
    EXCEPTION
      WHEN io.thecodeforge.error_pkg.e_fk_parent_not_found THEN
        v_caught := TRUE;
      WHEN OTHERS THEN
        fail('test_fk_parent_not_found_mapping',
             'WHEN OTHERS caught instead of e_fk_parent_not_found. SQLCODE: ' || SQLCODE);
    END;
    
    ROLLBACK;
    
    IF NOT v_caught THEN
      fail('test_fk_parent_not_found_mapping',
           'e_fk_parent_not_found did not fire for ORA-02291');
    END IF;
    
    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);
  BEGIN
    BEGIN
      SELECT customer_name INTO v_dummy
      FROM io.thecodeforge.customers
      WHERE customer_id = -999999;  -- guaranteed to not exist
    EXCEPTION
      WHEN io.thecodeforge.error_pkg.e_no_data_found THEN
        v_caught := TRUE;
      WHEN OTHERS THEN
        fail('test_no_data_found_mapping',
             'WHEN OTHERS caught instead of e_no_data_found. SQLCODE: ' || SQLCODE);
    END;
    
    IF NOT v_caught THEN
      fail('test_no_data_found_mapping',
           'e_no_data_found did not fire for ORA-01403');
    END IF;
    
    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;
  BEGIN
    BEGIN
      RAISE_APPLICATION_ERROR(-20001, 'Test business rule violation');
    EXCEPTION
      WHEN io.thecodeforge.error_pkg.e_business_rule_violation THEN
        v_caught := TRUE;
      WHEN OTHERS THEN
        fail('test_custom_error_range_mapping',
             'WHEN OTHERS caught instead of e_business_rule_violation. SQLCODE: ' || SQLCODE);
    END;
    
    IF NOT v_caught THEN
      fail('test_custom_error_range_mapping',
           'e_business_rule_violation did not fire for ORA-20001');
    END IF;
    
    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;
  BEGIN
    SELECT COUNT(*) INTO v_exists
    FROM dba_source
    WHERE owner = 'IO'
      AND name = 'ERROR_PKG'
      AND UPPER(text) LIKE '%E_SNAPSHOT_TOO_OLD%'
      AND UPPER(text) LIKE '%PRAGMA%';
    
    IF v_exists = 0 THEN
      fail('test_snapshot_too_old_is_declared',
           'e_snapshot_too_old not found in error_pkg — add PRAGMA EXCEPTION_INIT(e_snapshot_too_old, -1555)');
    END IF;
    
    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(SECOND FROM (SYSTIMESTAMP - v_start)) || 's'
    );
  EXCEPTION
    WHEN OTHERS THEN
      DBMS_OUTPUT.PUT_LINE('TEST SUITE FAILED: ' || SQLERRM);
      RAISE;  -- Fail the CI/CD step
  END 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 declarations
CREATE OR REPLACE PACKAGE 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 errors
CREATE OR REPLACE PROCEDURE io.thecodeforge.payment.validate_payment(
  p_customer_id IN NUMBER,
  p_amount      IN NUMBER
) IS
  v_limit   NUMBER;
  v_status  VARCHAR2(20);
BEGIN
  SELECT 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');
  END IF;
  
  IF p_amount > v_limit THEN
    RAISE_APPLICATION_ERROR(-20003,
      'Payment amount ' || p_amount ||
      ' exceeds credit limit ' || v_limit ||
      ' for account ' || p_customer_id);
  END IF;
EXCEPTION
  WHEN 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_payment
CREATE OR REPLACE PROCEDURE io.thecodeforge.order.create_order_with_payment(
  p_customer_id IN NUMBER,
  p_amount      IN NUMBER
) IS
BEGIN
  io.thecodeforge.payment.validate_payment(p_customer_id, p_amount);
  
  INSERT INTO io.thecodeforge.orders (
    customer_id, order_amount, status, created_at
  ) VALUES (
    p_customer_id, p_amount, 'PENDING', SYSTIMESTAMP
  );
  
  COMMIT;
EXCEPTION
  WHEN 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 raising
    ROLLBACK;
    
  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
ApproachReadabilityPerformanceMaintainabilityBest For
WHEN SQLCODE = -1403Low — requires mental translation from number to meaningFast — direct integer comparison at runtimePoor — error numbers scattered across procedures; no single search reveals all usagesQuick diagnostic scripts, throwaway anonymous blocks, one-off migrations
Named exception + PRAGMA EXCEPTION_INITHigh — self-documenting; intent is visible in the WHEN clauseFastest — binding resolved at compile time, no runtime evaluationExcellent — centralized in error_pkg; one location for all exception-to-error mappingsProduction systems, shared packages, team codebases, any code that survives beyond a sprint
WHEN OTHERS + SQLCODE branchMedium — readable only if CASE values are commented with ORA numbersEquivalent to SQLCODE comparison — runtime integer switchPoor — intent scattered in CASE branches; adding new errors requires finding all WHEN OTHERS blocksLegacy refactoring stages, temporary migration code, adapters wrapping third-party packages
WHEN OTHERS + SQLERRM string parsingLow — readable only with comments; fragile regex breaks on error message format changesSlowest — REGEXP_SUBSTR at runtime for every exceptionPoor — fragile across Oracle versions; SQLERRM message format is not a stable APIDynamic SQL constraint extraction, cross-environment constraint name differences, last-resort fallback
RAISE_APPLICATION_ERROR onlyMedium — custom messages are readable; error numbers still require a range allocation documentFast — direct raise, no dispatch overheadGood — messages are self-documenting if written well; still requires range documentation to be maintainableAPI boundary layers, input validation, business rule enforcement at schema entry points
GET STACKED DIAGNOSTICS / FORMAT_ERROR_STACKHigh — structured programmatic access to error metadataModerate — function call overhead per exceptionGood — programmatic; works with any error regardless of whether pragma was declaredLogging frameworks, centralized error tables, monitoring integrations, error analytics pipelines

Key takeaways

1
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.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Can one exception name be bound to multiple error numbers using multiple PRAGMA EXCEPTION_INIT statements?
02
Does PRAGMA EXCEPTION_INIT work with custom error numbers created by RAISE_APPLICATION_ERROR?
03
What is the performance difference between WHEN SQLCODE = -1403 and using PRAGMA EXCEPTION_INIT, and does it matter in practice?
04
How do I find all PRAGMA EXCEPTION_INIT declarations in my codebase?
05
Can PRAGMA EXCEPTION_INIT be used in anonymous PL/SQL blocks?
06
What happens to an ORA-06512 stack when a PRAGMA EXCEPTION_INIT exception is re-raised?
🔥

That's PL/SQL. Mark it forged?

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

Previous
How to Use RAISE_APPLICATION_ERROR Properly in Oracle
22 / 27 · PL/SQL
Next
SQL Developer Debugging Tutorial – Catch ORA-06512 Faster