PL/SQL Exception Handling: How to Write Bulletproof Code

Every database application will encounter unexpected situations — a record that does not exist, a constraint violation, a network timeout, a value that cannot be converted. How your PL/SQL code handles these situations determines whether your application is robust or fragile.

In this guide, we cover the full Oracle PL/SQL exception handling model: predefined exceptions, user-defined exceptions, SQLCODE and SQLERRM, RAISE_APPLICATION_ERROR, and the patterns that make exception handling genuinely useful in production code.

The Basic Exception Handling Structure

Every PL/SQL block can have an optional EXCEPTION section that catches and handles runtime errors:

BEGIN
  -- Normal executable code
  NULL;
  
EXCEPTION
  WHEN exception_name THEN
    -- Handle the exception
    NULL;
  WHEN OTHERS THEN
    -- Catch anything not handled above
    NULL;
END;

When an exception is raised (either by Oracle automatically or by your code using RAISE), execution jumps immediately to the EXCEPTION section. The matching WHEN clause runs. If no WHEN clause matches, the exception propagates up to the calling block.

Oracle’s Predefined Exceptions

Oracle provides named exceptions for the most common error situations. You can reference these directly in WHEN clauses:

Exception NameORA ErrorWhen It Occurs
NO_DATA_FOUNDORA-01403SELECT INTO returns zero rows
TOO_MANY_ROWSORA-01422SELECT INTO returns more than one row
ZERO_DIVIDEORA-01476Division by zero
DUP_VAL_ON_INDEXORA-00001Unique constraint violation
VALUE_ERRORORA-06502Type mismatch or value too large for variable
INVALID_NUMBERORA-01722Cannot convert string to number
CURSOR_ALREADY_OPENORA-06511OPEN called on an already-open cursor
INVALID_CURSORORA-01001Invalid cursor operation
TIMEOUT_ON_RESOURCEORA-00051Wait timeout for a resource
DECLARE
  v_salary employees.salary%TYPE;
BEGIN
  SELECT salary
  INTO   v_salary
  FROM   employees
  WHERE  employee_id = 9999; -- Does not exist
  
  DBMS_OUTPUT.PUT_LINE('Salary: ' || v_salary);
  
EXCEPTION
  WHEN NO_DATA_FOUND THEN
    DBMS_OUTPUT.PUT_LINE('Employee not found. Check the ID and try again.');
  WHEN TOO_MANY_ROWS THEN
    DBMS_OUTPUT.PUT_LINE('Query returned multiple rows. Refine your filter.');
  WHEN OTHERS THEN
    DBMS_OUTPUT.PUT_LINE('Unexpected error: ' || SQLERRM);
END;

SQLCODE and SQLERRM

Inside any exception handler, two functions give you information about the error:

  • SQLCODE — Returns the Oracle error number (negative for Oracle errors, +1 for user-defined exceptions without an assigned number, 100 for NO_DATA_FOUND)
  • SQLERRM — Returns the full error message text, including the ORA- error code
EXCEPTION
  WHEN OTHERS THEN
    -- Log the full error details
    INSERT INTO error_log (
      error_date, error_code, error_message, program_unit
    ) VALUES (
      SYSDATE, SQLCODE, SUBSTR(SQLERRM, 1, 500), 'PROCESS_ORDER'
    );
    COMMIT;
    RAISE; -- Re-raise the original exception
END;

Always SUBSTR SQLERRM to a safe length (500 characters is plenty) before inserting it into a VARCHAR2 column — the full message can be longer.

User-Defined Exceptions

You can define your own named exceptions for business logic errors:

DECLARE
  -- Define a user exception
  e_insufficient_credit EXCEPTION;
  v_balance             NUMBER;
  v_order_amount        NUMBER := 5000;
BEGIN
  SELECT credit_balance
  INTO   v_balance
  FROM   customer_accounts
  WHERE  customer_id = 101;
  
  -- Check business rule
  IF v_balance < v_order_amount THEN
    RAISE e_insufficient_credit; -- Raise our custom exception
  END IF;
  
  -- Process the order
  DBMS_OUTPUT.PUT_LINE('Order processed successfully.');
  
EXCEPTION
  WHEN e_insufficient_credit THEN
    DBMS_OUTPUT.PUT_LINE(
      'Order rejected: insufficient credit balance. ' ||
      'Balance: ' || v_balance || ', Required: ' || v_order_amount
    );
  WHEN NO_DATA_FOUND THEN
    DBMS_OUTPUT.PUT_LINE('Customer account not found.');
END;

RAISE_APPLICATION_ERROR: Meaningful Error Messages

RAISE_APPLICATION_ERROR lets you raise exceptions with a custom error number (between -20000 and -20999) and a descriptive message. This is the standard way to communicate business rule violations from PL/SQL to calling applications:

CREATE OR REPLACE PROCEDURE process_refund (
  p_order_id  IN NUMBER,
  p_amount    IN NUMBER
) IS
  v_order_status VARCHAR2(20);
  v_order_amount NUMBER;
BEGIN
  SELECT status, total_amount
  INTO   v_order_status, v_order_amount
  FROM   orders
  WHERE  order_id = p_order_id;
  
  -- Business validations
  IF v_order_status = 'CANCELLED' THEN
    RAISE_APPLICATION_ERROR(-20001, 
      'Refund denied: Order ' || p_order_id || ' is already cancelled.');
  END IF;
  
  IF p_amount > v_order_amount THEN
    RAISE_APPLICATION_ERROR(-20002, 
      'Refund amount (' || p_amount || ') exceeds original order total (' || 
      v_order_amount || ').');
  END IF;
  
  -- Process refund
  UPDATE orders SET refund_amount = p_amount WHERE order_id = p_order_id;
  COMMIT;
  
EXCEPTION
  WHEN NO_DATA_FOUND THEN
    RAISE_APPLICATION_ERROR(-20003, 'Order ID ' || p_order_id || ' not found.');
END;

The calling application receives the ORA-20001 (or -20002, -20003) error with your exact message, making it easy to display meaningful feedback to users.

Exception Propagation

Understanding how exceptions propagate is critical. If an exception is not handled in the current block, it propagates to the enclosing block:

BEGIN -- Outer block
  BEGIN -- Inner block
    SELECT salary INTO v_salary FROM employees WHERE employee_id = 9999;
  EXCEPTION
    WHEN NO_DATA_FOUND THEN
      DBMS_OUTPUT.PUT_LINE('Inner: Employee not found, continuing...');
      -- Exception is handled here. Outer block continues normally.
  END;
  
  DBMS_OUTPUT.PUT_LINE('Outer block: continuing after inner block handled exception.');
  
EXCEPTION
  WHEN OTHERS THEN
    -- Only reaches here if inner block does NOT handle the exception
    DBMS_OUTPUT.PUT_LINE('Outer: Unhandled exception: ' || SQLERRM);
END;

The RAISE Statement: Re-Raising Exceptions

Inside an exception handler, a bare RAISE (without an exception name) re-raises the current exception. This is the right pattern when you want to log an error but still let it propagate:

EXCEPTION
  WHEN OTHERS THEN
    -- Log the error
    log_error(SQLCODE, SQLERRM, 'CALCULATE_BONUS');
    -- Re-raise: let the caller know something went wrong
    RAISE;
END;

Associating Error Numbers with Named Exceptions

Some Oracle errors do not have predefined names but occur frequently enough that you want to catch them by name. Use EXCEPTION_INIT pragma:

DECLARE
  e_deadlock      EXCEPTION;
  e_table_missing EXCEPTION;
  PRAGMA EXCEPTION_INIT(e_deadlock,      -60);
  PRAGMA EXCEPTION_INIT(e_table_missing, -942);
BEGIN
  -- Your code here
  NULL;
EXCEPTION
  WHEN e_deadlock THEN
    DBMS_OUTPUT.PUT_LINE('Deadlock detected. Retry the transaction.');
  WHEN e_table_missing THEN
    DBMS_OUTPUT.PUT_LINE('Required table does not exist.');
END;

Exception Handling Best Practices

  • Always handle NO_DATA_FOUND when using SELECT INTO — Unhandled NO_DATA_FOUND in production is a very common bug
  • Never silently swallow exceptions — An empty WHEN OTHERS THEN NULL hides bugs and makes diagnosis impossible
  • Use RAISE_APPLICATION_ERROR for business rule violations — It gives calling code and users meaningful feedback
  • Log before you re-raise — Capture the context (SQLCODE, SQLERRM, program unit name, relevant IDs) in a log table before re-raising
  • Avoid WHEN OTHERS as the only handler — Be specific about the exceptions you expect. WHEN OTHERS is for truly unexpected scenarios
  • Never COMMIT inside a WHEN OTHERS handler — If an error occurred, committing partial work can corrupt data

A Production-Ready Error Logging Pattern

CREATE OR REPLACE PROCEDURE log_error (
  p_error_code    IN NUMBER,
  p_error_message IN VARCHAR2,
  p_program_unit  IN VARCHAR2,
  p_context       IN VARCHAR2 DEFAULT NULL
) IS
  PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
  INSERT INTO application_error_log (
    log_id, log_date, error_code, error_message, 
    program_unit, context_info, db_user, session_id
  ) VALUES (
    error_log_seq.NEXTVAL,
    SYSTIMESTAMP,
    p_error_code,
    SUBSTR(p_error_message, 1, 2000),
    p_program_unit,
    p_context,
    USER,
    USERENV('SESSIONID')
  );
  COMMIT; -- Autonomous transaction commits independently
END log_error;

The PRAGMA AUTONOMOUS_TRANSACTION is critical here — it lets the error log commit even if the calling transaction is rolled back. This ensures you never lose error information.

Conclusion

Solid exception handling is what separates prototype code from production-ready code. The difference between a system that crashes with "ORA-01403" and one that logs the error, provides a clear message, and continues gracefully comes down entirely to thoughtful exception handling.

Use predefined exceptions where they exist, define custom exceptions for business rules, always log before re-raising, and treat WHEN OTHERS as a last resort rather than a first line of defense. Your future self — and the developers who maintain your code — will thank you.

PreviousNext