【PL/SQL】例外処理完全ガイド|21種事前定義例外・WHEN OTHERS正しい使い方・BACKTRACE・自律トランザクション・実務パターン

【PL/SQL】例外処理の書き方と使い方|WHEN OTHERSの注意点も解説 PL/SQL

PL/SQLの例外処理は「エラーをキャッチして何となくログに出す」程度の実装で済ませると、本番で根本原因が永遠に追えないバグを生みます。特にWHEN OTHERS THEN NULL;WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE(SQLERRM);でエラーを握り潰すコードは、Oracle界隈で「Smothering Exception(例外窒息)」と呼ばれるアンチパターンの典型です。

多くの入門記事はBEGIN / EXCEPTION / ENDの基本構造と事前定義例外4〜5個までで止まり、実務で差が出る21種の事前定義例外全体像・PRAGMA EXCEPTION_INITでORAコードを命名・SQLCODESQLERRMDBMS_UTILITY.FORMAT_ERROR_BACKTRACEによる例外追跡・例外伝播スコープ・RAISERAISE exRAISE_APPLICATION_ERRORの違い・FORALL SAVE EXCEPTIONSでのバルク処理エラー・自律トランザクションログ・SAVEPOINTとの連携までカバーした情報源がほとんどありません。

この記事ではPL/SQLの例外処理を基礎から運用レベルまで徹底解説します。例外発生の仕組み、21種の事前定義例外、ユーザー定義例外の3方式(EXCEPTION/EXCEPTION_INIT/RAISE_APPLICATION_ERROR)、エラー情報の取り方(SQLCODESQLERRMFORMAT_ERROR_BACKTRACE)、Smothering Exceptionを防ぐWHEN OTHERS設計、自律トランザクションでのロバストなエラーログ、FORALL SAVE EXCEPTIONS、パッケージでの例外設計、実務パターン8選、アンチパターン8選まで2026年版の決定版です。関連する【PL/SQL】IF文完全ガイドと組み合わせて活用してください。

この記事で学べること

  • 例外処理の全体像:発生→伝播→キャッチ→復旧/再RAISEのライフサイクル
  • 事前定義例外21種一覧とORAコードのマッピング表
  • ユーザー定義例外の3方式EXCEPTION宣言/PRAGMA EXCEPTION_INITRAISE_APPLICATION_ERROR
  • エラー情報の取り方SQLCODESQLERRMDBMS_UTILITY.FORMAT_ERROR_STACKFORMAT_ERROR_BACKTRACE
  • RAISERAISE exRAISE_APPLICATION_ERROR使い分け
  • 例外の伝播スコープ(ネストブロック/サブプログラム/パッケージ跨ぎ)
  • WHEN OTHERS正しい使い方(Smothering Exception回避)
  • 自律トランザクション(PRAGMA AUTONOMOUS_TRANSACTION)でエラーログを確実に保存
  • SAVEPOINT+例外処理での部分ロールバック
  • バルク処理のFORALL SAVE EXCEPTIONS1件エラーで全体を止めない
  • パッケージでの例外カタログ設計-20000-20999の割り当て)
  • 実務パターン8選+アンチパターン8選
スポンサーリンク

例外処理のライフサイクル:発生→伝播→キャッチ

PL/SQL例外処理の全体像
例外発生
  ↓(同じブロックにEXCEPTION句があれば)
EXCEPTION句で WHEN 例外名 THEN をマッチング
  ↓(マッチしたらブロック終了、しなければ)
呼び出し元のブロックへ伝播
  ↓
親ブロックのEXCEPTION句でマッチング
  ↓(すべて不発なら)
クライアント側(SQL*Plus/JDBC/アプリ)へエラー返却

重要:マッチして処理した後は「処理済み」とみなされ、
     そのブロックは正常終了扱いで親へ戻る(伝播しない)
最小の例外処理
DECLARE
  v_count NUMBER;
BEGIN
  SELECT COUNT(*) INTO v_count FROM users WHERE id = 9999;

  IF v_count = 0 THEN
    RAISE_APPLICATION_ERROR(-20001, 'ユーザーが見つかりません');
  END IF;
EXCEPTION
  WHEN NO_DATA_FOUND THEN
    DBMS_OUTPUT.PUT_LINE('データなし: ' || SQLERRM);
  WHEN TOO_MANY_ROWS THEN
    DBMS_OUTPUT.PUT_LINE('複数行検出: ' || SQLERRM);
  WHEN OTHERS THEN
    DBMS_OUTPUT.PUT_LINE('予期せぬエラー: ' || SQLCODE || ' / ' || SQLERRM);
    RAISE;    -- 握りつぶさず親へ伝播
END;
/

3つの鉄則:①具体的な例外を先に、WHEN OTHERSは最後の安全網、②WHEN OTHERSでは必ずSQLCODESQLERRMを記録、③予期しないエラーはRAISE;で再送出して呼び出し元に伝える。

事前定義例外21種:完全対応表

PL/SQLにはOracleが組み込みで提供する例外が21種類あり、明示的な宣言なしでWHEN 例外名で受け取れます。

例外名 ORAコード 発生シーン
NO_DATA_FOUND ORA-01403 SELECT INTOで0件
TOO_MANY_ROWS ORA-01422 SELECT INTOで2行以上
ZERO_DIVIDE ORA-01476 0除算
DUP_VAL_ON_INDEX ORA-00001 UNIQUE制約違反
VALUE_ERROR ORA-06502 型変換・桁あふれ
INVALID_NUMBER ORA-01722 文字列→数値変換失敗(SQL)
INVALID_CURSOR ORA-01001 未OPENカーソル操作
CURSOR_ALREADY_OPEN ORA-06511 既にOPENのカーソルを再OPEN
CASE_NOT_FOUND ORA-06592 CASE文でELSE無し+マッチなし
COLLECTION_IS_NULL ORA-06531 未初期化コレクション操作
SUBSCRIPT_BEYOND_COUNT ORA-06533 コレクション範囲外index
SUBSCRIPT_OUTSIDE_LIMIT ORA-06532 VARRAYの上限超え
NOT_LOGGED_ON ORA-01012 DB未接続でSQL実行
LOGIN_DENIED ORA-01017 認証失敗
STORAGE_ERROR ORA-06500 PGAメモリ不足
PROGRAM_ERROR ORA-06501 内部エラー
ROWTYPE_MISMATCH ORA-06504 型不一致代入
SELF_IS_NULL ORA-30625 オブジェクト型メソッドをnullで呼出
TIMEOUT_ON_RESOURCE ORA-00051 リソース待ちタイムアウト
SYS_INVALID_ROWID ORA-01410 無効なROWID指定
ACCESS_INTO_NULL ORA-06530 未初期化オブジェクトのメンバ参照

よく使うのはこの5つ

  • NO_DATA_FOUND:SELECT INTOで0件
  • TOO_MANY_ROWS:SELECT INTOで複数件
  • DUP_VAL_ON_INDEX:UNIQUE制約違反(INSERT/UPDATE)
  • VALUE_ERROR:型変換・桁あふれ
  • ZERO_DIVIDE:0除算

事前定義例外の使用例

典型パターン
DECLARE
  v_name users.name%TYPE;
BEGIN
  SELECT name INTO v_name FROM users WHERE id = p_id;
  -- 通常処理
EXCEPTION
  WHEN NO_DATA_FOUND THEN
    RAISE_APPLICATION_ERROR(-20001, 'ID=' || p_id || ' のユーザーが見つかりません');
  WHEN TOO_MANY_ROWS THEN
    RAISE_APPLICATION_ERROR(-20002, 'ID=' || p_id || ' で複数ユーザー検出 (データ不整合)');
  WHEN OTHERS THEN
    DBMS_OUTPUT.PUT_LINE('予期せぬエラー: ' || SQLCODE || ' / ' || SQLERRM);
    RAISE;
END;

ユーザー定義例外の3方式

方式①:EXCEPTION宣言+RAISE

シンプルなユーザー定義例外
DECLARE
  e_invalid_amount EXCEPTION;
  v_amount NUMBER := -100;
BEGIN
  IF v_amount < 0 THEN
    RAISE e_invalid_amount;
  END IF;
EXCEPTION
  WHEN e_invalid_amount THEN
    DBMS_OUTPUT.PUT_LINE('金額がマイナス');
END;

方式②:PRAGMA EXCEPTION_INIT(ORAコードに名前付け)

ORAコードを例外名に紐付け
-- 特定のORAエラーを例外名で扱えるようにする
DECLARE
  -- ORA-02292(外部キー参照整合性違反)に名前付け
  e_child_record_found EXCEPTION;
  PRAGMA EXCEPTION_INIT(e_child_record_found, -2292);

  -- ORA-02291(親キー不在)
  e_parent_not_found EXCEPTION;
  PRAGMA EXCEPTION_INIT(e_parent_not_found, -2291);

  -- ORA-00054(リソースビジー)
  e_resource_busy EXCEPTION;
  PRAGMA EXCEPTION_INIT(e_resource_busy, -54);
BEGIN
  DELETE FROM departments WHERE id = p_dept_id;
EXCEPTION
  WHEN e_child_record_found THEN
    RAISE_APPLICATION_ERROR(-20101, '子レコードが存在するため削除できません');
  WHEN e_resource_busy THEN
    RAISE_APPLICATION_ERROR(-20102, 'ロック待ちでタイムアウト、少し待って再試行してください');
END;

EXCEPTION_INITの真価:ORAコードをそのままWHEN OTHERS THEN IF SQLCODE = -2292 THEN ...と分岐させるより、例外名として明示するほうが可読性が格段に上。過去のORAコードを名前付きで扱えるためパッケージ・トリガーの保守性が向上します。

方式③:RAISE_APPLICATION_ERROR(呼び出し元へエラー通知)

アプリへメッセージ付きでエラー返却
BEGIN
  -- ビジネスルール違反を例外として通知
  IF :new.salary < 0 THEN
    RAISE_APPLICATION_ERROR(-20001, '給与はマイナスにできません');
  END IF;
  
  -- エラー番号は -20000〜-20999 の範囲(ユーザー定義専用)
  -- メッセージは2048バイトまで
  -- 第3引数TRUEでエラースタック保持
  IF :new.department_id NOT IN (10, 20, 30) THEN
    RAISE_APPLICATION_ERROR(
      -20002,
      '無効な部門ID: ' || :new.department_id,
      TRUE    -- エラースタックに積む
    );
  END IF;
END;

3方式の使い分け

方式 用途 エラー番号
EXCEPTION宣言 ブロック内部で完結する例外 なし(内部のみ)
PRAGMA EXCEPTION_INIT ORAコードを名前付きで扱いたい Oracle既定の負数
RAISE_APPLICATION_ERROR 呼び出し元へメッセージ付き通知 -20000 〜 -20999

エラー情報の取得:SQLCODE/SQLERRM/BACKTRACE

4つの必須関数
EXCEPTION
  WHEN OTHERS THEN
    -- ORAエラー番号(負数)、ユーザー定義は-20000〜-20999
    DBMS_OUTPUT.PUT_LINE('SQLCODE: ' || SQLCODE);

    -- エラーメッセージ
    DBMS_OUTPUT.PUT_LINE('SQLERRM: ' || SQLERRM);

    -- エラースタック(複数エラーを全て含む)
    DBMS_OUTPUT.PUT_LINE('STACK:'  || CHR(10) || DBMS_UTILITY.FORMAT_ERROR_STACK);

    -- エラー発生行まで辿れるトレースバック(重要!)
    DBMS_OUTPUT.PUT_LINE('TRACE:'  || CHR(10) || DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);

    -- 呼び出しスタック(12cR1以降)
    DBMS_OUTPUT.PUT_LINE('CALL:'  || CHR(10) || DBMS_UTILITY.FORMAT_CALL_STACK);
END;

出力例:BACKTRACEで行番号まで特定

実行例
-- コード
CREATE OR REPLACE PROCEDURE proc_a IS
BEGIN
  proc_b();       -- Line 3
END;
/

CREATE OR REPLACE PROCEDURE proc_b IS
  v NUMBER;
BEGIN
  v := 1/0;       -- Line 4: ZERO_DIVIDE
END;
/

-- 呼び出し側
BEGIN
  proc_a();
EXCEPTION
  WHEN OTHERS THEN
    DBMS_OUTPUT.PUT_LINE(DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);
END;

-- 出力
-- ORA-06512: at "SCHEMA.PROC_B", line 4
-- ORA-06512: at "SCHEMA.PROC_A", line 3
-- ORA-06512: at line 2
-- ↑どの行でエラーが起きたか完全に追跡できる

BACKTRACEを取るタイミング:DBMS_UTILITY.FORMAT_ERROR_BACKTRACE最後にRAISEされた地点のバックトレースを返します。ネストしたブロックで再RAISEすると上書きされるため、最初のキャッチ地点で必ず取得してログに残すのが鉄則。取り損ねると本番で原因追跡不能に陥ります。

RAISE/RAISE ex/RAISE_APPLICATION_ERROR の違い

構文 使う場所 効果
RAISE;(裸) EXCEPTION句内のみ 現在キャッチ中の例外を再送出
RAISE 例外名; BEGIN〜END内のどこでも 指定例外を発生させる
RAISE_APPLICATION_ERROR(-20000〜-20999, msg) BEGIN〜END内のどこでも メッセージ付きで呼び出し元に通知
再送出パターン(握り潰しを回避)
BEGIN
  -- 重い処理
  process_data();
EXCEPTION
  WHEN NO_DATA_FOUND THEN
    -- 既知の例外はビジネスロジックで処理
    log_and_continue();

  WHEN OTHERS THEN
    -- 予期しないエラーはログを残してから再送出
    INSERT INTO error_log(error_time, error_code, error_msg, error_backtrace)
    VALUES (SYSDATE, SQLCODE, SQLERRM, DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);
    COMMIT;
    RAISE;    -- 重要:握り潰さない
END;

RAISE(裸)とRAISE exceptionの違い:RAISE;(裸)は現在キャッチ中の例外をそのまま再送出し、BACKTRACE/SQLCODE/SQLERRMがすべて保持されます。対してRAISE e_custom;新しい例外に置き換わるため元の発生地点の情報が失われます(デバッグ困難に)。予期せぬエラーの再送出は必ずRAISE;(裸)で。

WHEN OTHERSの正しい使い方(Smothering Exception回避)

WHEN OTHERSは強力な「最後の安全網」ですが、間違って使うとすべての原因不明エラーを隠蔽して本番で深刻な問題を引き起こします。

❌ やってはいけないパターン(Smothering Exception)

エラーを握り潰す典型
-- ❌ 最悪:何もしない
EXCEPTION
  WHEN OTHERS THEN
    NULL;    -- 例外窒息(Smothering)
END;

-- ❌ ログを出すだけで握り潰す
EXCEPTION
  WHEN OTHERS THEN
    DBMS_OUTPUT.PUT_LINE('エラー: ' || SQLERRM);
    -- ↑ DBMS_OUTPUTは本番では消えることが多い
    -- 呼び出し元には何も伝わらず、正常終了として処理される
END;

-- ❌ 部分的な情報しか残さない
EXCEPTION
  WHEN OTHERS THEN
    INSERT INTO error_log(msg) VALUES (SQLERRM);
    -- ↑ SQLCODE/BACKTRACE無し → どの行でエラーか不明
END;

⭕ 正しいパターン(完全な情報保存+再送出)

Log & Reraiseパターン
EXCEPTION
  -- 既知の例外は個別処理
  WHEN NO_DATA_FOUND THEN
    RAISE_APPLICATION_ERROR(-20001, 'データが存在しません');

  WHEN DUP_VAL_ON_INDEX THEN
    RAISE_APPLICATION_ERROR(-20002, '既に登録済みです');

  -- 未知の例外は完全にログして再送出
  WHEN OTHERS THEN
    pkg_error_log.log_error(
      p_code      => SQLCODE,
      p_message   => SQLERRM,
      p_backtrace => DBMS_UTILITY.FORMAT_ERROR_BACKTRACE,
      p_context   => 'proc_xxx(id=' || p_id || ')'
    );
    RAISE;    -- ← 最重要:呼び出し元へ伝える
END;

WHEN OTHERS の3つの絶対ルール:RAISE;またはRAISE_APPLICATION_ERRORで例外を外に出す、②SQLCODESQLERRMFORMAT_ERROR_BACKTRACEを全て記録、③具体例外を先にWHENで捕まえ、OTHERSは最後の網にする。この3つが守られないPL/SQLは本番で障害が起きた時に原因が一生分からないコードになります。

自律トランザクションで確実にエラーログを保存

例外発生後にROLLBACKすると、エラーログ自体もロールバックされて失われる問題を、PRAGMA AUTONOMOUS_TRANSACTIONで解決します。

自律トランザクション版ログプロシージャ
CREATE OR REPLACE PROCEDURE log_error(
  p_code      IN NUMBER,
  p_message   IN VARCHAR2,
  p_backtrace IN VARCHAR2,
  p_context   IN VARCHAR2 DEFAULT NULL
) IS
  PRAGMA AUTONOMOUS_TRANSACTION;  -- 重要:独立したトランザクション
BEGIN
  INSERT INTO error_log(
    error_time, error_code, error_msg,
    error_backtrace, context, username, client_info
  ) VALUES (
    SYSTIMESTAMP, p_code, p_message,
    p_backtrace, p_context, USER, SYS_CONTEXT('USERENV', 'CLIENT_INFO')
  );
  COMMIT;    -- 独立COMMITで確定(呼び出し元がROLLBACKしても残る)
EXCEPTION
  WHEN OTHERS THEN
    ROLLBACK;
    -- ログ保存すら失敗する最悪ケース(DB障害等)
    -- ここでは握り潰さず呼び出し元に伝える
    RAISE;
END;
/
呼び出し側
BEGIN
  UPDATE orders SET status = 'processed' WHERE id = p_id;
  process_payment(p_id);
  COMMIT;
EXCEPTION
  WHEN OTHERS THEN
    log_error(
      p_code      => SQLCODE,
      p_message   => SQLERRM,
      p_backtrace => DBMS_UTILITY.FORMAT_ERROR_BACKTRACE,
      p_context   => 'process_order(' || p_id || ')'
    );
    ROLLBACK;    -- ビジネスデータはロールバック
    -- ↑ ログは自律トランザクションで既にCOMMIT済みなので残る
    RAISE;
END;

エラーログテーブル設計の推奨スキーマ:error_time TIMESTAMPerror_code NUMBERerror_msg VARCHAR2(4000)error_backtrace CLOBcontext VARCHAR2(1000)usernamesession_idclient_info。パーティション(月単位)+30〜90日で自動削除のライフサイクル管理が実用的。

SAVEPOINTで部分ロールバック

例外発生時にすべてをロールバックするのは過剰なケースでは、SAVEPOINTで途中地点に戻せます。大量データの一括処理で「失敗レコードだけスキップ」する時などに有用。

SAVEPOINTによる部分復帰
DECLARE
  v_success_count NUMBER := 0;
  v_error_count   NUMBER := 0;
BEGIN
  FOR rec IN (SELECT id, amount FROM orders_queue) LOOP
    SAVEPOINT sp_order;
    BEGIN
      UPDATE accounts SET balance = balance - rec.amount WHERE id = rec.account_id;
      INSERT INTO transactions(order_id, amount) VALUES (rec.id, rec.amount);
      v_success_count := v_success_count + 1;
    EXCEPTION
      WHEN OTHERS THEN
        ROLLBACK TO sp_order;    -- このレコードだけロールバック
        v_error_count := v_error_count + 1;
        log_error(
          p_code     => SQLCODE,
          p_message  => SQLERRM,
          p_context  => 'order_id=' || rec.id
        );
    END;
  END LOOP;

  COMMIT;    -- 成功分だけ確定
  DBMS_OUTPUT.PUT_LINE('成功: ' || v_success_count || ', 失敗: ' || v_error_count);
END;

SAVEPOINT使い所

SAVEPOINTはバッチ処理の部分エラー許容が典型用途。さらに詳しい使い方は【PL/SQL】SAVEPOINTを使った部分ロールバックの実装方法参照。1万件のINSERTで10件失敗しても9990件は保存される、等の堅牢な設計が可能に。

FORALL SAVE EXCEPTIONS:バルク処理のエラー個別処理

FORALLは1万件を1回のRDBMSラウンドトリップで処理する高速機能ですが、1件でもエラーが出ると全体停止するのが難点。SAVE EXCEPTIONS付きにすると「失敗した行だけスキップして処理続行」が可能です。

FORALL SAVE EXCEPTIONS
DECLARE
  TYPE t_amounts IS TABLE OF NUMBER;
  v_ids    t_amounts := t_amounts(1, 2, 3, 4, 5);
  v_amounts t_amounts := t_amounts(100, -50, 200, 300, NULL);
  --                                         ↑負数      ↑NULL

  e_bulk_errors EXCEPTION;
  PRAGMA EXCEPTION_INIT(e_bulk_errors, -24381);
BEGIN
  FORALL i IN 1..v_ids.COUNT SAVE EXCEPTIONS
    UPDATE accounts SET balance = balance + v_amounts(i)
    WHERE id = v_ids(i);

  DBMS_OUTPUT.PUT_LINE('全件成功');
EXCEPTION
  WHEN e_bulk_errors THEN
    -- SQL%BULK_EXCEPTIONSで失敗レコードを個別処理
    DBMS_OUTPUT.PUT_LINE('エラー件数: ' || SQL%BULK_EXCEPTIONS.COUNT);
    FOR i IN 1..SQL%BULK_EXCEPTIONS.COUNT LOOP
      DBMS_OUTPUT.PUT_LINE(
        'iteration ' || SQL%BULK_EXCEPTIONS(i).ERROR_INDEX ||
        ' → ORA-' || LPAD(SQL%BULK_EXCEPTIONS(i).ERROR_CODE, 5, '0') ||
        ': ' || SQLERRM(-SQL%BULK_EXCEPTIONS(i).ERROR_CODE)
      );
    END LOOP;
    COMMIT;    -- 成功分だけ確定
END;

バルク処理の詳細

バルク処理(BULK COLLECTFORALL)の使い方は【PL/SQL】バルク処理で高速化!FORALLとBULK COLLECTの使い方、エラー処理特化は【PL/SQL】FORALLとSAVE EXCEPTIONSでバルクDMLのエラーを個別処理する方法を参照。

パッケージでの例外設計:エラー番号カタログ

プロジェクト全体で-20000〜-20999の1000枠をどう割り当てるか、事前に例外カタログを整備すると保守性が劇的に向上します。

例外カタログパッケージ
CREATE OR REPLACE PACKAGE pkg_errors IS
  -- ユーザー/認証系 -20001〜-20099
  ERR_USER_NOT_FOUND        CONSTANT NUMBER := -20001;
  ERR_DUPLICATE_EMAIL       CONSTANT NUMBER := -20002;
  ERR_INVALID_PASSWORD      CONSTANT NUMBER := -20003;

  -- 注文/決済系 -20100〜-20199
  ERR_ORDER_NOT_FOUND       CONSTANT NUMBER := -20100;
  ERR_INSUFFICIENT_BALANCE  CONSTANT NUMBER := -20101;
  ERR_INVALID_AMOUNT        CONSTANT NUMBER := -20102;

  -- バリデーション系 -20200〜-20299
  ERR_REQUIRED_FIELD        CONSTANT NUMBER := -20200;
  ERR_INVALID_FORMAT        CONSTANT NUMBER := -20201;

  -- ヘルパー
  PROCEDURE raise_error(
    p_code IN NUMBER,
    p_msg  IN VARCHAR2
  );
END pkg_errors;
/

CREATE OR REPLACE PACKAGE BODY pkg_errors IS
  PROCEDURE raise_error(
    p_code IN NUMBER,
    p_msg  IN VARCHAR2
  ) IS
  BEGIN
    RAISE_APPLICATION_ERROR(p_code, p_msg, TRUE);
  END;
END pkg_errors;
/
呼び出し側
PROCEDURE create_user(p_email VARCHAR2, p_password VARCHAR2) IS
  v_count NUMBER;
BEGIN
  IF p_email IS NULL THEN
    pkg_errors.raise_error(pkg_errors.ERR_REQUIRED_FIELD, 'メールアドレスは必須');
  END IF;

  IF LENGTH(p_password) < 8 THEN
    pkg_errors.raise_error(pkg_errors.ERR_INVALID_PASSWORD, 'パスワードは8文字以上');
  END IF;

  SELECT COUNT(*) INTO v_count FROM users WHERE email = p_email;
  IF v_count > 0 THEN
    pkg_errors.raise_error(pkg_errors.ERR_DUPLICATE_EMAIL, '既に登録済み: ' || p_email);
  END IF;

  INSERT INTO users(email, password_hash) VALUES (p_email, hash_pw(p_password));
END;

エラー番号カタログの効果:①定数名で意図が明確(ERR_DUPLICATE_EMAILは誰が見ても分かる)、②プロジェクトドキュメントとしても使える、③アプリ側(Java/Python)で同じ定数を共有するとエラーコード管理が統一、④-20000〜-20099を機能グループごとに100ずつ割り当てると拡張しやすい。

実務パターン8選

①セーフ検索(NO_DATA_FOUND対応)

0件でもエラーにしない
FUNCTION get_user_name(p_id NUMBER) RETURN VARCHAR2 IS
  v_name users.name%TYPE;
BEGIN
  SELECT name INTO v_name FROM users WHERE id = p_id;
  RETURN v_name;
EXCEPTION
  WHEN NO_DATA_FOUND THEN
    RETURN NULL;    -- 0件ならNULLを返す仕様
  -- TOO_MANY_ROWSは発生させない(主キー検索だから)
END;

②冪等なUPSERT(DUP_VAL_ON_INDEX対応)

2回実行しても安全
PROCEDURE upsert_user(p_email VARCHAR2, p_name VARCHAR2) IS
BEGIN
  INSERT INTO users(email, name) VALUES (p_email, p_name);
EXCEPTION
  WHEN DUP_VAL_ON_INDEX THEN
    -- 既存なら更新
    UPDATE users SET name = p_name WHERE email = p_email;
END;
-- MERGE文使う方が性能は良いが、単発INSERT/UPDATEならこのパターンもOK

③処理の中断&ロールバック

例外で中断+ROLLBACK+ログ+再送出
BEGIN
  UPDATE orders SET status = 'processing' WHERE id = p_id;
  call_external_api(p_id);
  COMMIT;
EXCEPTION
  WHEN OTHERS THEN
    log_error(SQLCODE, SQLERRM, DBMS_UTILITY.FORMAT_ERROR_BACKTRACE, 'process_order');
    ROLLBACK;    -- 不整合を残さない
    RAISE;       -- 呼び出し元(アプリ)に通知
END;

④リソース確実クローズ

Cursor/ファイルのcleanup
DECLARE
  v_cursor SYS_REFCURSOR;
BEGIN
  OPEN v_cursor FOR 'SELECT ... ';
  -- 処理
  CLOSE v_cursor;
EXCEPTION
  WHEN OTHERS THEN
    IF v_cursor%ISOPEN THEN
      CLOSE v_cursor;
    END IF;
    RAISE;
END;

⑤入力バリデーション一括

複数のエラーを蓄積して返す
DECLARE
  v_errors VARCHAR2(4000);
BEGIN
  IF p_email IS NULL THEN v_errors := v_errors || 'メール必須; '; END IF;
  IF p_age IS NULL OR p_age < 18 THEN v_errors := v_errors || '年齢要件NG; '; END IF;
  IF LENGTH(p_password) < 8 THEN v_errors := v_errors || 'パスワード桁数NG; '; END IF;

  IF v_errors IS NOT NULL THEN
    RAISE_APPLICATION_ERROR(-20200, '入力エラー: ' || v_errors);
  END IF;

  INSERT INTO users VALUES (p_email, p_age, hash_pw(p_password));
END;

⑥トリガー内の条件的エラー

BEFORE INSERTで妥当性チェック
CREATE OR REPLACE TRIGGER trg_validate_order
BEFORE INSERT OR UPDATE ON orders
FOR EACH ROW
BEGIN
  IF :NEW.amount IS NULL OR :NEW.amount <= 0 THEN
    RAISE_APPLICATION_ERROR(-20102, '注文金額は正の数');
  END IF;

  IF :NEW.status NOT IN ('pending', 'paid', 'shipped', 'delivered') THEN
    RAISE_APPLICATION_ERROR(-20103, '無効なステータス: ' || :NEW.status);
  END IF;
END;

⑦外部キーエラーをユーザー向けに翻訳

ORAコード→フレンドリーメッセージ
DECLARE
  e_parent_not_found EXCEPTION;
  PRAGMA EXCEPTION_INIT(e_parent_not_found, -2291);
  e_child_found EXCEPTION;
  PRAGMA EXCEPTION_INIT(e_child_found, -2292);
BEGIN
  INSERT INTO orders(user_id, amount) VALUES (p_user_id, p_amount);
EXCEPTION
  WHEN e_parent_not_found THEN
    RAISE_APPLICATION_ERROR(-20300, 'ユーザーID=' || p_user_id || ' が存在しません');
END;

⑧バッチの部分成功(SAVEPOINTパターン)

1万件中、失敗した10件だけスキップ
FOR rec IN (SELECT id, data FROM import_queue) LOOP
  SAVEPOINT sp_item;
  BEGIN
    process_item(rec.id, rec.data);
  EXCEPTION
    WHEN OTHERS THEN
      ROLLBACK TO sp_item;
      log_error(SQLCODE, SQLERRM, DBMS_UTILITY.FORMAT_ERROR_BACKTRACE,
                'import_id=' || rec.id);
  END;
END LOOP;
COMMIT;

アンチパターン8選とリファクタリング

WHEN OTHERS THEN NULL;。Oracleコミュニティで最悪のアンチパターン。例外を完全に握り潰し、本番で何が起きても気づけない。必ずRAISE;RAISE_APPLICATION_ERRORで通知。

WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE(SQLERRM);。本番環境ではDBMS_OUTPUTバッファが消費されず/読まれないため実質握り潰し。永続テーブルへのINSERT+RAISEを組み合わせる。

RAISE e_customで元の例外を置き換える。BACKTRACEが失われて原因追跡不能。RAISE;(裸)で情報保持。

SQLERRMだけログに記録。SQLCODEとBACKTRACEがないと行番号・呼び出し元が分からない。必ずDBMS_UTILITY.FORMAT_ERROR_BACKTRACEを併記。

⑤例外ハンドラ内でCOMMIT。ビジネスロジック中のEXCEPTIONでCOMMITすると「中途半端な状態」で確定される。ログ保存は自律トランザクション(PRAGMA AUTONOMOUS_TRANSACTION)で分離。

⑥事前定義例外を独自例外と同じ名前にするNO_DATA_FOUND EXCEPTION;のように再宣言するとOracle既定の例外が隠蔽され、SELECT INTOの0件が握り潰される。独自例外はe_プレフィックス等で区別。

WHEN OTHERSを最初に書く。WHEN句は上から評価されるため、具体的な例外が来てもOTHERSに吸われる。具体例外→一般例外→OTHERSの順に書くのが鉄則。

⑧エラーメッセージに機密情報を載せるRAISE_APPLICATION_ERROR(-20001, 'SELECT ... WHERE password=' || :pwd)等はアプリのエラーログに流出。メッセージは外向け情報のみ、詳細は内部ログに記録。

よくある質問

QWHEN OTHERSは使うべき?
A使うべきですが必ずRAISE;RAISE_APPLICATION_ERRORで再送出すること。「未知の例外を記録しつつ握り潰さない最後の安全網」として機能します。WHEN OTHERS THEN NULL;は最悪のアンチパターン。
QROLLBACKは自動?
APL/SQL内の例外発生でトランザクションは自動ロールバックしません。明示的にROLLBACK;を書くか、呼び出し元のアプリケーション層/EXIT時に自動ロールバックに委ねます。例外ハンドラ内で必ずROLLBACKするかは設計方針次第。
QSQLERRMとFORMAT_ERROR_BACKTRACEの違い
ASQLERRMエラーメッセージのみ(例:ORA-01476: divisor is zero)、DBMS_UTILITY.FORMAT_ERROR_BACKTRACE発生した行番号と呼び出し階層を含む詳細スタックトレース。本番デバッグには必ずBACKTRACEも記録すること。
QRAISEとRAISE exceptionの違いは?
ARAISE;(裸)は現在キャッチ中の例外をそのまま再送出し、SQLCODE/SQLERRM/BACKTRACEが保持されます。RAISE e_custom;新しい例外として発生し元の情報は失われます。予期せぬエラーの再送出は必ずRAISE;で。
Qエラー番号はどの範囲を使える?
A-20000〜-20999の1000枠がユーザー定義専用。これ以外はRAISE_APPLICATION_ERRORの第1引数で使うとエラー。プロジェクトで機能ごとに範囲を決め(例:ユーザー系-20000〜、注文系-20100〜)、カタログパッケージで定数化するのが実用的。
Q事前定義例外の一覧はどこで見れる?
AOracle公式ドキュメントPL/SQL Language ReferenceまたはシステムビューSTANDARDパッケージ。本記事「事前定義例外21種」の表で主要なものをすべて網羅しています。
QCASE文のELSE省略で例外が出る
APL/SQLのCASE文(Statement)はELSE省略でどのWHENにも該当しないとCASE_NOT_FOUND(ORA-06592)例外発生。解決は必ずELSE NULL;などを書くこと。SQLのCASE式はELSE省略時NULLを返す別挙動。IF文/CASE文の詳細は【PL/SQL】IF文完全ガイド参照。
Qトリガー内で例外を発生させるには?
ARAISE_APPLICATION_ERROR(-20xxx, 'メッセージ')を使います。トリガー内で通常の例外をRAISEすると呼び出し元のDMLが失敗し、呼び出し元アプリにエラーコード/メッセージが返ります。アプリケーション層でエラーメッセージを表示する用途に最適です。
Q自律トランザクションで何ができる?
A呼び出し元のCOMMITROLLBACK独立したトランザクションを開始できます。代表的な使い方は「ビジネスロジックはROLLBACKしてもエラーログは残す」。詳細は【PL/SQL】AUTONOMOUS TRANSACTIONで独立した処理を行う方法参照。
Qバルク処理で1件だけエラーで全体止まる
AFORALL ... SAVE EXCEPTIONSを使うと失敗行だけスキップして処理続行できます。エラー詳細はSQL%BULK_EXCEPTIONSで個別取得。詳しくは【PL/SQL】FORALLとSAVE EXCEPTIONSでバルクDMLのエラーを個別処理する方法参照。

関連記事

まとめ

  • 例外ライフサイクル:発生→同ブロックのEXCEPTIONでマッチ→未マッチなら親へ伝播
  • 事前定義例外21種:NO_DATA_FOUND/TOO_MANY_ROWS/DUP_VAL_ON_INDEX/VALUE_ERROR/ZERO_DIVIDEが頻出
  • ユーザー定義3方式:EXCEPTION宣言/PRAGMA EXCEPTION_INIT/RAISE_APPLICATION_ERROR
  • エラー情報4種:SQLCODESQLERRMFORMAT_ERROR_STACKFORMAT_ERROR_BACKTRACE(行番号必須)
  • RAISE;(裸)は情報保持再送出RAISE eは情報失う→裸推奨
  • RAISE_APPLICATION_ERROR(-20000〜-20999, msg)でアプリ層へメッセージ通知
  • WHEN OTHERS最後の網+ログ+RAISE;が3原則(Smothering回避)
  • PRAGMA AUTONOMOUS_TRANSACTIONでログ保存を独立COMMIT(ROLLBACKで消えない)
  • SAVEPOINTROLLBACK TO部分復帰が可能
  • FORALL SAVE EXCEPTIONSバルク処理の失敗行だけスキップ
  • エラー番号カタログパッケージで-20000〜-20999の1000枠を機能別に割当
  • アンチパターン:WHEN OTHERS THEN NULL/DBMS_OUTPUTだけ/RAISE eで置換/SQLERRMのみログ/ハンドラ内COMMIT/事前定義名の再宣言/OTHERSを最初/機密情報メッセージ

PL/SQLの例外処理は「エラーを受ける」だけでなく、発生地点を完全に記録し、適切な粒度で呼び出し元へ通知することまで含めた設計が本質です。本記事の21種事前定義例外リファレンス、3方式のユーザー定義、自律トランザクション+SAVEPOINT+FORALL SAVE EXCEPTIONSを組み合わせれば、本番で原因追跡が困難なコードからいつでもデバッグできる堅牢なコードへと進化できます。IF文はIF文完全ガイド、バルク処理はFORALL/BULK COLLECT、自律トランザクションはAUTONOMOUS TRANSACTION、SAVEPOINTはSAVEPOINT部分ロールバックと合わせてご活用ください。