PL/SQLで業務処理を作っていると、例外を投げる場面は多くあります。在庫不足、締め済み期間への登録、権限不足、入力値の不備、外部連携の失敗など、単にRAISE_APPLICATION_ERROR(-20001, 'error')と書くだけでは、画面、API、バッチ、ログで扱いにくいエラーになります。
業務エラーコード設計では、エラー番号、業務コード、画面表示メッセージ、ログ用詳細、HTTPステータス、再試行可否などを整理します。これを共通パッケージ化しておくと、アプリケーションやバッチが同じルールでエラーを扱えます。例外処理の基本は PL/SQL例外処理完全ガイド、再試行や一時障害の考え方は PL/SQLの例外設計と再試行パターン も参考になります。
-20000番台の使い分け- 業務エラーとシステムエラーの分離
- メッセージテーブルの設計
RAISE_APPLICATION_ERRORのラッパーパッケージ- 画面/API向けメッセージとログ用詳細の分離
- バッチログ、JSONエラー、多言語対応との連携
業務エラーコード設計で解決したい問題
エラー設計がないまま開発が進むと、同じ意味のエラーに別々の番号が割り当てられたり、逆に同じ番号が複数の意味で使われたりします。また、画面に出してよいメッセージと、ログにだけ残すべき詳細が混ざると、ユーザーに内部情報を見せてしまう危険もあります。
-20001だけでは、入力エラーなのか権限エラーなのか判断できません。業務エラーコードは、単なる番号表ではありません。DB内の例外、APIレスポンス、バッチログ、監査ログ、運用問い合わせをつなぐ共通語彙です。最初に小さくても体系を決めておくと、後からエラーを増やしても破綻しにくくなります。
業務エラーとシステムエラーを分ける
まず、ユーザーや業務ルールに起因するエラーと、システム障害に近いエラーを分けます。在庫不足や締め済み期間への登録は、業務として想定できるエラーです。一方で、表が存在しない、権限がない、DB接続が切れた、予期しないNULL参照などは、利用者に細かい内部情報を見せるべきではありません。
PL/SQLの例外処理では、WHEN OTHERSで何でも業務エラーに変換しないことが重要です。想定済みの業務エラーだけを共通パッケージから投げ、想定外の例外はバックトレース付きでログに残します。
-20000番台の割り当て方
RAISE_APPLICATION_ERRORで使えるエラー番号は、基本的に-20000から-20999です。この範囲を無計画に使うとすぐに衝突するので、機能領域ごとに番号帯を分けます。
番号帯は例です。大切なのは、番号を見たときにどの領域のエラーか分かること、そして新しいエラーを追加する場所が迷子にならないことです。既存システムに番号規約がある場合は、それを優先します。
メッセージテーブルを設計する
エラー文言をPL/SQLコードに直書きすると、文言変更のたびに再デプロイが必要になります。実務では、エラーコード、Oracleエラー番号、表示メッセージ、ログ用テンプレート、HTTPステータス、再試行可否をメッセージテーブルで管理すると扱いやすくなります。
CREATE TABLE app_error_message (
error_code VARCHAR2(50) NOT NULL,
locale VARCHAR2(10) DEFAULT 'ja' NOT NULL,
oracle_error_no NUMBER NOT NULL,
severity VARCHAR2(20) DEFAULT 'ERROR' NOT NULL,
user_message VARCHAR2(1000) NOT NULL,
log_message VARCHAR2(2000),
http_status NUMBER DEFAULT 400 NOT NULL,
retryable CHAR(1) DEFAULT 'N' NOT NULL,
enabled CHAR(1) DEFAULT 'Y' NOT NULL,
updated_by VARCHAR2(128) DEFAULT SYS_CONTEXT('USERENV','SESSION_USER') NOT NULL,
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
CONSTRAINT pk_app_error_message PRIMARY KEY (error_code, locale),
CONSTRAINT ck_app_error_no CHECK (oracle_error_no BETWEEN -20999 AND -20000),
CONSTRAINT ck_app_error_severity CHECK (severity IN ('INFO','WARN','ERROR','FATAL')),
CONSTRAINT ck_app_error_retry CHECK (retryable IN ('Y','N')),
CONSTRAINT ck_app_error_enabled CHECK (enabled IN ('Y','N'))
);
CREATE UNIQUE INDEX uq_app_error_message_no
ON app_error_message(oracle_error_no, locale);
user_messageは画面やAPIレスポンスに返してよい文言です。log_messageは運用者や開発者向けの詳細です。DBの内部名、SQL、スタックトレース、個人情報などをユーザー向け文言に混ぜないよう、列を分けておくのがポイントです。
初期メッセージを登録する
メッセージテーブルは作っただけでは意味がないので、エラーコードの命名規則と初期データをセットで管理します。コードは英大文字とアンダースコアで、APIやログから機械的に扱いやすい名前にします。
INSERT INTO app_error_message (
error_code, locale, oracle_error_no, severity,
user_message, log_message, http_status, retryable
) VALUES (
'ORDER_CLOSED_PERIOD', 'ja', -20101, 'ERROR',
'締め済み期間のため登録できません。',
'order_date={1}, closing_period={2}',
409, 'N'
);
INSERT INTO app_error_message (
error_code, locale, oracle_error_no, severity,
user_message, log_message, http_status, retryable
) VALUES (
'ORDER_STOCK_SHORTAGE', 'ja', -20102, 'ERROR',
'在庫が不足しています。',
'item_code={1}, requested_qty={2}, available_qty={3}',
409, 'N'
);
INSERT INTO app_error_message (
error_code, locale, oracle_error_no, severity,
user_message, log_message, http_status, retryable
) VALUES (
'EXTERNAL_TIMEOUT', 'ja', -20910, 'WARN',
'外部システムとの通信に失敗しました。時間をおいて再実行してください。',
'destination={1}, timeout_seconds={2}',
503, 'Y'
);
番号とコードの両方を持つ理由は、PL/SQL例外としては番号が必要で、APIやアプリケーションでは文字列コードのほうが扱いやすいためです。たとえば-20101はOracle側の例外番号、ORDER_CLOSED_PERIODは画面やAPIで使う業務コードとして扱います。
RAISE_APPLICATION_ERRORを直接呼ばない
各プロシージャから直接RAISE_APPLICATION_ERRORを書くと、番号、文言、ログの形式がばらつきます。共通パッケージを作り、エラーコードを指定して投げる形に寄せると、メッセージ取得やログ連携を集中管理できます。
CREATE OR REPLACE PACKAGE app_error AS
PROCEDURE raise_error(
p_error_code VARCHAR2,
p_arg1 VARCHAR2 DEFAULT NULL,
p_arg2 VARCHAR2 DEFAULT NULL,
p_arg3 VARCHAR2 DEFAULT NULL,
p_locale VARCHAR2 DEFAULT 'ja'
);
FUNCTION message_of(
p_error_code VARCHAR2,
p_locale VARCHAR2 DEFAULT 'ja'
) RETURN VARCHAR2;
FUNCTION current_code RETURN VARCHAR2;
END;
/
CREATE OR REPLACE PACKAGE BODY app_error AS
g_current_code VARCHAR2(50);
FUNCTION replace_args(
p_template VARCHAR2,
p_arg1 VARCHAR2,
p_arg2 VARCHAR2,
p_arg3 VARCHAR2
) RETURN VARCHAR2 IS
BEGIN
RETURN REPLACE(
REPLACE(
REPLACE(p_template, '{1}', NVL(p_arg1, '')),
'{2}', NVL(p_arg2, '')),
'{3}', NVL(p_arg3, ''));
END;
FUNCTION message_of(
p_error_code VARCHAR2,
p_locale VARCHAR2
) RETURN VARCHAR2 IS
v_message app_error_message.user_message%TYPE;
BEGIN
SELECT user_message
INTO v_message
FROM app_error_message
WHERE error_code = p_error_code
AND locale = p_locale
AND enabled = 'Y';
RETURN v_message;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN '処理を完了できませんでした。';
END;
PROCEDURE raise_error(
p_error_code VARCHAR2,
p_arg1 VARCHAR2,
p_arg2 VARCHAR2,
p_arg3 VARCHAR2,
p_locale VARCHAR2
) IS
v_no app_error_message.oracle_error_no%TYPE;
v_message app_error_message.user_message%TYPE;
BEGIN
g_current_code := p_error_code;
SELECT oracle_error_no, user_message
INTO v_no, v_message
FROM app_error_message
WHERE error_code = p_error_code
AND locale = p_locale
AND enabled = 'Y';
RAISE_APPLICATION_ERROR(
v_no,
p_error_code || ': ' || replace_args(v_message, p_arg1, p_arg2, p_arg3),
TRUE
);
EXCEPTION
WHEN NO_DATA_FOUND THEN
g_current_code := 'UNKNOWN_ERROR_CODE';
RAISE_APPLICATION_ERROR(-20999, 'UNKNOWN_ERROR_CODE: ' || p_error_code, TRUE);
END;
FUNCTION current_code RETURN VARCHAR2 IS
BEGIN
RETURN g_current_code;
END;
END;
/
第三引数にTRUEを渡すと、既存のエラースタックに追加する動きになります。原因例外を完全に消したくない場合に有効です。一方、利用者へ見せる文言を完全に置き換えたい場合はFALSEを使う選択もあります。どちらの場合も、ユーザーへ返すメッセージに内部スタックをそのまま出さないよう、APIや画面側では別途整形します。
業務処理から使う
業務処理では、番号や文言を直接書かず、エラーコードだけを指定します。こうしておくと、文言変更や多言語対応をしても業務ロジックを変えずに済みます。
CREATE OR REPLACE PROCEDURE register_order(
p_order_date DATE,
p_item_code VARCHAR2,
p_qty NUMBER
) AS
v_closing_period DATE;
v_available_qty NUMBER;
BEGIN
v_closing_period := DATE '2026-05-31';
IF p_order_date <= v_closing_period THEN
app_error.raise_error(
p_error_code => 'ORDER_CLOSED_PERIOD',
p_arg1 => TO_CHAR(p_order_date, 'YYYY-MM-DD'),
p_arg2 => TO_CHAR(v_closing_period, 'YYYY-MM-DD')
);
END IF;
SELECT stock_qty
INTO v_available_qty
FROM item_stock
WHERE item_code = p_item_code;
IF v_available_qty < p_qty THEN
app_error.raise_error(
p_error_code => 'ORDER_STOCK_SHORTAGE',
p_arg1 => p_item_code,
p_arg2 => TO_CHAR(p_qty),
p_arg3 => TO_CHAR(v_available_qty)
);
END IF;
-- 注文登録処理
END;
/
業務ロジック側は、ORDER_CLOSED_PERIODやORDER_STOCK_SHORTAGEという意味のあるコードだけを扱います。これにより、エラー番号や文言の管理を共通パッケージに閉じ込められます。
ログ用詳細を残す
ユーザー向けメッセージは短く安全にし、調査用の詳細はログへ残します。DBMS_APPLICATION_INFOやバッチログと組み合わせると、どの処理でどの業務エラーが発生したか追いやすくなります。処理名やアクションを残す考え方は インストゥルメンテーション設計 と相性がよいです。
CREATE TABLE app_error_log (
log_id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
error_code VARCHAR2(50),
oracle_error_no NUMBER,
module_name VARCHAR2(128),
action_name VARCHAR2(128),
user_message VARCHAR2(1000),
log_detail VARCHAR2(4000),
backtrace CLOB,
created_by VARCHAR2(128) DEFAULT SYS_CONTEXT('USERENV','SESSION_USER') NOT NULL,
created_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
);
CREATE INDEX ix_app_error_log_created
ON app_error_log(created_at, error_code);
CREATE OR REPLACE PROCEDURE write_error_log(
p_error_code VARCHAR2,
p_oracle_error_no NUMBER,
p_user_message VARCHAR2,
p_detail VARCHAR2,
p_backtrace CLOB
) AS
BEGIN
INSERT INTO app_error_log(
error_code,
oracle_error_no,
module_name,
action_name,
user_message,
log_detail,
backtrace
) VALUES (
p_error_code,
p_oracle_error_no,
SYS_CONTEXT('USERENV','MODULE'),
SYS_CONTEXT('USERENV','ACTION'),
p_user_message,
p_detail,
p_backtrace
);
END;
/
ログを必ず残したいからといって、安易にAUTONOMOUS_TRANSACTIONを使うと、本処理がロールバックされたのにログだけ残る設計になります。それが望ましい監査ログなのか、同一トランザクションで消えるべき処理ログなのかを決めて使い分けます。独立トランザクションの注意点は トランザクション設計パターン でも扱っています。
API向けJSONエラーに変換する
REST APIからPL/SQLを呼ぶ場合、Oracleのエラー番号や日本語文だけを返すより、構造化されたJSONエラーにすると扱いやすくなります。API設計の詳細は 例外をJSON形式で返すREST対応API設計 とつながります。
CREATE OR REPLACE FUNCTION error_json(
p_error_code VARCHAR2,
p_locale VARCHAR2 DEFAULT 'ja'
) RETURN CLOB IS
v_msg app_error_message%ROWTYPE;
BEGIN
SELECT *
INTO v_msg
FROM app_error_message
WHERE error_code = p_error_code
AND locale = p_locale
AND enabled = 'Y';
RETURN JSON_OBJECT(
'ok' VALUE false,
'error' VALUE JSON_OBJECT(
'code' VALUE v_msg.error_code,
'message' VALUE v_msg.user_message,
'severity' VALUE v_msg.severity,
'retryable' VALUE CASE v_msg.retryable WHEN 'Y' THEN true ELSE false END
)
RETURNING CLOB
);
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN JSON_OBJECT(
'ok' VALUE false,
'error' VALUE JSON_OBJECT(
'code' VALUE 'UNKNOWN_ERROR',
'message' VALUE '処理を完了できませんでした。',
'retryable' VALUE false
)
RETURNING CLOB
);
END;
/
APIではmessageだけでなく、codeとretryableを返すとクライアント側が判断しやすくなります。たとえばORDER_STOCK_SHORTAGEなら画面で数量修正を促し、EXTERNAL_TIMEOUTなら再実行ボタンや時間をおいた再試行を案内できます。
バッチログと連携する
バッチ処理では、画面表示よりも再実行可否、処理件数、失敗理由の集計が重要になります。エラーコードをバッチ実行履歴に残しておくと、異常検知や運用通知で使いやすくなります。ジョブ実行履歴の考え方は ジョブ実行履歴の集中管理と異常検知 が参考になります。
BEGIN
DBMS_APPLICATION_INFO.SET_MODULE('ORDER_IMPORT_BATCH', 'load_orders');
order_import_main.run;
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE BETWEEN -20999 AND -20000 THEN
write_error_log(
p_error_code => NVL(app_error.current_code, 'BUSINESS_ERROR'),
p_oracle_error_no => SQLCODE,
p_user_message => SQLERRM,
p_detail => 'business error in order import',
p_backtrace => DBMS_UTILITY.FORMAT_ERROR_BACKTRACE
);
ELSE
write_error_log(
p_error_code => 'UNEXPECTED_ERROR',
p_oracle_error_no => SQLCODE,
p_user_message => '処理中に予期しないエラーが発生しました。',
p_detail => SQLERRM,
p_backtrace => DBMS_UTILITY.FORMAT_ERROR_BACKTRACE
);
END IF;
RAISE;
END;
/
ログ連携では、SQLERRMを文字列解析してエラーコードを取り出す設計は避けます。メッセージ形式変更で壊れやすいため、業務エラーコードはパッケージ変数やログ引数として明示的に渡します。想定外エラーでは、ユーザー向け文言を汎用化し、詳細なSQLERRMとバックトレースはログ側に閉じ込めます。
多言語対応と文言変更
メッセージテーブルにlocaleを持たせておくと、日本語、英語などの文言を切り替えられます。ただし、業務エラーコード自体は言語に依存しない固定値にします。文言だけを変え、クライアントやログ分析で使うerror_codeは変えないのが基本です。
INSERT INTO app_error_message (
error_code, locale, oracle_error_no, severity,
user_message, log_message, http_status, retryable
) VALUES (
'ORDER_STOCK_SHORTAGE', 'en', -20102, 'ERROR',
'Insufficient stock.',
'item_code={1}, requested_qty={2}, available_qty={3}',
409, 'N'
);
運用で文言を変えたい場合は、誰がいつ変更したかを残します。設定値やFeature Flagと同じく、メッセージも本番挙動に影響するデータです。変更履歴や権限設計は 設定値管理テーブルとFeature Flag設計 の考え方を流用できます。
やってはいけない設計
エラーコード設計は、細かくしすぎても雑すぎても運用しにくくなります。最初に避けたいパターンを決めておくと、開発者ごとのばらつきを減らせます。
設計チェックリスト
新しい業務エラーを追加するときは、次の観点で確認します。このチェックをレビュー観点にしておくと、エラー体系が崩れにくくなります。
ERROR_001ではなく、ORDER_CLOSED_PERIODのように読める名前にします。-20000番台を使います。まとめ
PL/SQLの業務エラーコード設計では、RAISE_APPLICATION_ERRORをただ使うだけでは不十分です。エラー番号、業務コード、ユーザー向け文言、ログ用詳細、HTTPステータス、再試行可否を整理し、共通パッケージから投げる形にすると、画面、API、バッチ、ログで一貫した扱いができます。
特に重要なのは、業務エラーとシステムエラーを分けること、-20000番台を機能領域ごとに割り当てること、文言をメッセージテーブルで管理すること、内部情報をユーザーに返さないことです。エラーコードは障害対応時の共通語彙になります。最初に小さな体系を決めておけば、機能追加やAPI化、バッチ運用が進んでも、エラーの意味を追いやすいPL/SQL基盤にできます。

