Using PRAGMA EXCEPTION_INIT to Map Oracle Errors
- 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
- Centralize all declarations in io.thecodeforge.error_pkg specification β one source of truth eliminates scattered numeric SQLCODE comparisons and inconsistent exception naming
- 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
- 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
Need to find all PRAGMA EXCEPTION_INIT declarations in the codebase
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;Need to generate PRAGMA EXCEPTION_INIT declarations for all unique constraints
-- 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;Exception handler catches wrong error or named exception stops firing after DDL change
-- 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;Production Incident
Production Debug GuideSymptom-to-action mapping for pragma declaration, compilation, and runtime behavior issues
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.
-- =================================================== -- 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; /
- 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
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.
-- =================================================== -- 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; /
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.
-- =================================================== -- 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;
- 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
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.
-- =================================================== -- 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; /
- 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
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).
-- =================================================== -- 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; /
- 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
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 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; /
| 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 | Quick diagnostic scripts, throwaway anonymous blocks, one-off migrations |
| Named exception + PRAGMA EXCEPTION_INIT | High β self-documenting; intent is visible in the WHEN clause | Fastest β binding resolved at compile time, no runtime evaluation | Excellent β centralized in error_pkg; one location for all exception-to-error mappings | Production systems, shared packages, team codebases, any code that survives beyond a sprint |
| WHEN OTHERS + SQLCODE branch | Medium β readable only if CASE values are commented with ORA numbers | Equivalent to SQLCODE comparison β runtime integer switch | Poor β intent scattered in CASE branches; adding new errors requires finding all WHEN OTHERS blocks | Legacy refactoring stages, temporary migration code, adapters wrapping third-party packages |
| WHEN OTHERS + SQLERRM string parsing | Low β readable only with comments; fragile regex breaks on error message format changes | Slowest β REGEXP_SUBSTR at runtime for every exception | Poor β fragile across Oracle versions; SQLERRM message format is not a stable API | Dynamic SQL constraint extraction, cross-environment constraint name differences, last-resort fallback |
| RAISE_APPLICATION_ERROR only | Medium β custom messages are readable; error numbers still require a range allocation document | Fast β direct raise, no dispatch overhead | Good β messages are self-documenting if written well; still requires range documentation to be maintainable | API boundary layers, input validation, business rule enforcement at schema entry points |
| GET STACKED DIAGNOSTICS / FORMAT_ERROR_STACK | High β structured programmatic access to error metadata | Moderate β function call overhead per exception | Good β programmatic; works with any error regardless of whether pragma was declared | Logging frameworks, centralized error tables, monitoring integrations, error analytics pipelines |
π― Key Takeaways
- 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
- Centralize all declarations in io.thecodeforge.error_pkg specification β one source of truth eliminates scattered numeric SQLCODE comparisons and inconsistent exception naming
- 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
- Always use negative error numbers β positive values compile without error but bind silently fails; only a unit test will catch this
- 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
- 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
Interview Questions on This Topic
- QHow would you refactor a legacy PL/SQL codebase with 200+ procedures that use WHEN SQLCODE = comparisons throughout to use PRAGMA EXCEPTION_INIT instead?SeniorReveal
- QWhat is the difference between PRAGMA EXCEPTION_INIT and RAISE_APPLICATION_ERROR, and when would you use each?Mid-levelReveal
- QHow do you handle the situation where multiple unique constraints all produce ORA-00001 and you need to distinguish them in a single exception handler?SeniorReveal
- QWhat happens if you declare PRAGMA EXCEPTION_INIT with a positive error number instead of negative, and how would you detect this in production?JuniorReveal
- QExplain when PRAGMA EXCEPTION_INIT is insufficient and what you use instead.SeniorReveal
Frequently Asked Questions
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.
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.
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.
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.
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.
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.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.