Skip to content
Homeβ€Ί Databaseβ€Ί Using PRAGMA EXCEPTION_INIT to Map Oracle Errors

Using PRAGMA EXCEPTION_INIT to Map Oracle Errors

Where developers are forged. Β· Structured learning Β· Free forever.
πŸ“ Part of: PL/SQL β†’ Topic 22 of 27
Advanced technique to give meaningful names to Oracle errors instead of seeing generic ORA-06512.
βš™οΈ Intermediate β€” basic Database knowledge assumed
In this tutorial, you'll learn
Advanced technique to give meaningful names to Oracle errors instead of seeing generic ORA-06512.
  • 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
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
⚑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
🚨 START HERE
PRAGMA EXCEPTION_INIT Quick Reference
Copy-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 ActionQuery 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 NowCreate 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 ActionQuery 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 NowCopy 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 ActionVerify 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 NowAfter 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.
Production IncidentUndiagnosable Constraint Violation in Payment ProcessingA payment reconciliation batch failed with ORA-06512 but the error handler used SQLCODE = -1, which matched 14 different constraint types. Engineers spent 3 hours determining which constraint was violated while duplicate settlement records accumulated.
SymptomBatch 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.
AssumptionThe 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 causeThe 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.
Fix1. 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 diagnosisPRAGMA EXCEPTION_INIT with constraint-specific names enables instant identification without SQLERRM parsing during incidentsCentralize exception declarations in a shared package so the whole team uses consistent namesLog and continue is only safe if you log enough context to reconstruct what was skipped β€” constraint name, offending key values, and record identifier at minimumGenerate 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 issues
PLS-00702: pragma must be declared in declarative part→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.
Named exception compiles without error but never fires — errors fall through to WHEN OTHERS→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%'.
Named exception declared in package body is not visible in other packages→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.
Exception fires but SQLERRM returns ORA-06510: PL/SQL: unhandled user-defined exception instead of the original Oracle error→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.
PRAGMA EXCEPTION_INIT for -20001 to -20999 range does not fire for RAISE_APPLICATION_ERROR calls→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.
Named exception fires correctly in development but falls through to WHEN OTHERS in production→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 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.sql Β· SQL
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
-- ===================================================
-- 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;
/
Mental Model
Pragma as Compile-Time Binding β€” Not Runtime Logic
Think of PRAGMA EXCEPTION_INIT as an entry in the compiler's exception dispatch table. At compilation, Oracle records: 'if error -1403 propagates through this block, treat it as e_no_customer.' At runtime, there is no lookup β€” the compiler already wired it.
  • 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.sql Β· SQL
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
-- ===================================================
-- 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
πŸ“Š 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.sql Β· SQL
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
-- ===================================================
-- 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.sql Β· SQL
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
-- ===================================================
-- 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.sql Β· SQL
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
-- ===================================================
-- 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.sql Β· SQL
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
-- ===================================================
-- 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
πŸ“Š 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.
πŸ—‚ Error Handling Approaches Comparison
Trade-offs between different Oracle exception handling patterns β€” choose based on your context, not on what was easiest to write
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

  • 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

    βœ•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 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
    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.
  • QWhat is the difference between PRAGMA EXCEPTION_INIT and RAISE_APPLICATION_ERROR, and when would you use each?Mid-levelReveal
    They solve different problems and work together, not in competition. PRAGMA EXCEPTION_INIT is a compile-time directive that gives a readable name to an existing Oracle error number β€” built-in errors like ORA-01403, ORA-00001, or even custom -20001 errors. It makes the WHEN clause self-documenting. RAISE_APPLICATION_ERROR is a runtime function that creates new error conditions in the -20000 to -20999 range with custom messages β€” it defines what new errors mean for your application. The production pattern is: use RAISE_APPLICATION_ERROR to raise business-specific errors at enforcement points, then use PRAGMA EXCEPTION_INIT to give those same error numbers readable names in every package that catches them. One defines the error; the other names it for the catcher.
  • 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
    You cannot have Oracle dispatch selectively to different named exceptions in the same EXCEPTION block when they are all bound to the same error number β€” the first matching WHEN clause wins, regardless of which constraint fired. The solutions are: (1) design procedures so each one can only violate one specific constraint β€” then the handler is unambiguous by construction; (2) for procedures that unavoidably touch multiple constraints, catch the generic e_unique_violation and use REGEXP_SUBSTR on SQLERRM to extract the constraint name, then branch with IF/ELSIF; (3) wrap each INSERT in its own sub-block with its own handler, so each sub-block's context makes the constraint implicit. The constraint name extraction approach is the fallback, not the primary design. If you find yourself writing a 10-way IF/ELSIF on constraint names, the procedure is doing too many things and should be decomposed.
  • 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
    The declaration compiles without error or warning β€” the compiler accepts it. The binding silently fails. The named exception will never fire because Oracle error codes are negative integers by convention, and the compiler does not validate that the error number you provided is a real Oracle error. In production, errors that should be caught by the named exception fall through to WHEN OTHERS or propagate uncaught. Detection requires a unit test that deliberately raises the bound error and asserts the named exception fired. Without that test, you will not know until an incident reveals that your handler did not execute. Always follow every pragma declaration with a test β€” compilation success is not evidence that the binding works.
  • QExplain when PRAGMA EXCEPTION_INIT is insufficient and what you use instead.SeniorReveal
    Two scenarios. First: dynamic SQL built at runtime, where you cannot know at compile time which tables or constraints the generated statement will touch. PRAGMA EXCEPTION_INIT requires compile-time knowledge of the error. For dynamic SQL, catch the generic named exception (e_unique_violation) and extract the constraint name from SQLERRM or DBMS_UTILITY.FORMAT_ERROR_STACK using REGEXP_SUBSTR at runtime. Always log the SQL string alongside the error. Second: cross-environment deployments where constraint names differ between development, staging, and production. Named exceptions bound to specific constraint names (which all share -1 anyway) do not help here β€” you need runtime constraint name extraction from SQLERRM. The correct architecture uses both: PRAGMA EXCEPTION_INIT for the error category (unique violation, FK violation, etc.) and SQLERRM parsing for the specific constraint within that category.

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.

πŸ”₯
Naren Founder & Author

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.

← PreviousHow to Use RAISE_APPLICATION_ERROR Properly in OracleNext β†’SQL Developer Debugging Tutorial – Catch ORA-06512 Faster
Forged with πŸ”₯ at TheCodeForge.io β€” Where Developers Are Forged