AIによる要約
例外クラス名は、どの処理で落ちたかではなく、何が成立しなかったかを表すと使いやすくなります。業務ルール違反にはInsufficientStockExceptionのような具体名を、外部APIやDBなど技術的な失敗にはExternalApiTimeoutExceptionのような原因が分かる名前を付けます。BusinessExceptionやSystemExceptionだけで済ませず、呼び出し側が判断できる粒度を残すことが重要です。


業務システムでは、在庫不足、二重申請、締め日超過、外部APIのタイムアウトなど、多くの失敗を扱います。新人のうちは「とりあえずRuntimeExceptionを継承したクラスを作る」ところまでで終わりがちですが、曖昧な名前は後から調査コストを増やします。
この記事では、SIer・SESのJava案件で使いやすい例外名の決め方を、注文処理の実例で整理します。難しい例外設計ではなく、まずレビューで説明できる基準を持つことが目的です。
この記事のポイント
- 例外名は「処理名」より「成立しなかった条件」を表す
- 業務例外と技術的な例外を同じクラスへ詰め込まない
- メッセージには調査に必要な識別子を残し、機密情報は入れない
- 細かすぎる継承階層より、呼び出し側が判断できる粒度を優先する
例外クラス名で伝えるべきこと
良い例外名は、catchする側やログを読む人へ「何が失敗したか」を伝えます。次の名前を見比べてください。
// 何が起きたか分かりにくい
throw new OrderException();
throw new BusinessException();
throw new SystemException();
// 成立しなかった条件が分かる
throw new InsufficientStockException(productId, requestedQuantity);
throw new OrderAlreadyCanceledException(orderId);
throw new ExternalApiTimeoutException("payment-api", timeoutMillis);
OrderExceptionは注文に関する失敗だとしか分かりません。在庫不足なのか、注文が見つからないのか、状態遷移が不正なのかを、結局メッセージやスタックトレースから探すことになります。
「どのServiceで発生したか」ではなく、「何ができなかったか」を例外名にします。
業務例外とシステム例外を分けて考える
現場では例外を大きく、業務ルール上の失敗と、技術的な失敗に分けると整理しやすくなります。厳密な定義はプロジェクトごとに違いますが、まず次の感覚を持っておけば十分です。
| 種類 | 例 | 画面・APIでの扱い |
|---|---|---|
| 業務例外 | 在庫不足、申請済み、取消期限超過 | 利用者が理解できるメッセージへ変換しやすい |
| システム例外 | DB接続失敗、外部APIタイムアウト、設定不足 | 共通エラーにし、運用ログへ詳細を残すことが多い |
業務例外の命名例
InsufficientStockException: 在庫が必要数に足りないOrderAlreadyCanceledException: 取消済み注文を再度取り消したApprovalDeadlineExceededException: 承認期限を過ぎているDuplicateApplicationException: 同じ申請がすでに存在する
業務例外は、画面で利用者へ返すメッセージと近いことがあります。ただし、例外名を日本語メッセージの代わりにせず、表示文言はメッセージコードなどで別管理する案件も多くあります。
システム例外の命名例
ExternalApiTimeoutException: 外部APIが規定時間内に応答しないFileImportException: ファイル取込処理が完了できないConfigurationNotFoundException: 必須設定が見つからないDataAccessException: Springが提供するDBアクセス例外の共通体系
ライブラリやフレームワークが適切な例外をすでに提供しているなら、自作例外で包む必要があるかを先に確認します。すべてをSystemExceptionへ変換すると、元の例外型から得られた情報を失うことがあります。
例外名を決める4つの手順
1. 失敗した業務条件を文章にする
最初から英語名を考えず、「要求数に対して在庫が足りない」「取消済み注文をもう一度取り消した」のように日本語で状況を書きます。文章が曖昧なら、例外名も曖昧になります。
2. 名詞または状態をExceptionの前へ置く
NotFound、AlreadyCanceled、InsufficientStock、DeadlineExceededなど、失敗の状態を短く表します。プロジェクト内ですでに使われている語彙があれば揃えます。
3. 呼び出し側が別対応するか確認する
在庫不足と注文不存在で、返すHTTPステータスや画面メッセージが違うなら、別クラスにする意味があります。どちらも同じ共通エラーにしかならないのに、似た例外を20個作る必要はありません。
4. 既存コードとの一貫性を確認する
NotFoundExceptionとNotExistExceptionが混在すると検索しにくくなります。新しい命名規則を独断で増やさず、近い機能の例外、コーディング規約、ControllerAdviceを確認します。
注文処理の実装例
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final StockRepository stockRepository;
public void cancel(long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
if (order.isCanceled()) {
throw new OrderAlreadyCanceledException(orderId);
}
order.cancel();
}
}
注文不存在と取消済みでは、発生条件も利用者へ伝える内容も違います。例外型を分けると、呼び出し側で個別に変換できます。
public class OrderNotFoundException extends RuntimeException {
private final long orderId;
public OrderNotFoundException(long orderId) {
super("Order not found. orderId=" + orderId);
this.orderId = orderId;
}
public long getOrderId() {
return orderId;
}
}
IDをフィールドとして持たせれば、文字列を解析せずにエラーレスポンスやログへ利用できます。ただし、必要な情報まで何でも例外へ詰め込むのではなく、呼び出し側が使う値へ絞ります。
メッセージには調査用の情報を残す
Failedだけのメッセージでは、運用担当者が再現条件を探せません。注文ID、商品ID、外部システム名など、事象を絞れる情報を含めます。
throw new InsufficientStockException(
"Insufficient stock. productId=" + productId
+ ", requested=" + requestedQuantity
+ ", available=" + availableQuantity);
パスワード、アクセストークン、クレジットカード番号、個人情報を例外メッセージへ出してはいけません。ログへ出力される前提で、識別に必要な最小限の情報にします。
checked例外とRuntimeExceptionは名前だけで決めない
Exceptionを直接継承するchecked例外は、呼び出し側へcatchまたはthrowsを強制します。RuntimeExceptionはその強制がありません。業務例外だから必ずchecked、システム例外だから必ずRuntimeExceptionという決まりではありません。
Springの業務アプリケーションではRuntimeException系へ統一する設計もよくあります。トランザクションのロールバック方針にも関係するため、新人が単独で決めず、既存プロジェクトの規約と共通例外処理を確認してください。
継承階層は浅く保つ
共通処理のためにBusinessExceptionを親にすること自体は問題ではありません。ただし、ApplicationException、BusinessException、OrderException、OrderStateExceptionと階層を深くすると、どの型でcatchすべきか分かりにくくなります。
public abstract class BusinessException extends RuntimeException {
private final String messageCode;
protected BusinessException(String messageCode, String message) {
super(message);
this.messageCode = messageCode;
}
public String getMessageCode() {
return messageCode;
}
}
public class InsufficientStockException extends BusinessException {
public InsufficientStockException(long productId) {
super("order.stock.insufficient",
"Insufficient stock. productId=" + productId);
}
}
共通のメッセージコードやログ方針が必要なら、親クラスを1段設ける程度から始めます。継承は分類のために使い、名前の曖昧さを隠すために使わないことが大切です。
ControllerAdviceで利用者向けの応答へ変換する
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(OrderNotFoundException.class)
public ResponseEntity<ApiError> handleOrderNotFound(
OrderNotFoundException ex) {
ApiError error = new ApiError("ORDER_NOT_FOUND", "注文が見つかりません");
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(InsufficientStockException.class)
public ResponseEntity<ApiError> handleInsufficientStock(
InsufficientStockException ex) {
ApiError error = new ApiError("INSUFFICIENT_STOCK", "在庫が不足しています");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
}
例外メッセージをそのまま画面へ返すと、内部情報が漏れる可能性があります。例外は調査情報を保持し、ControllerAdviceでは利用者向けの安全な文言へ変換します。
現場レビューでよくある指摘
// レビューコメント例 OrderExceptionだけでは発生条件が分かりません。 在庫不足と注文不存在で呼び出し側の対応が違うため、例外型を分けてください。 // レビューコメント例 元例外を捨てているため、原因追跡が難しくなっています。 ラップする場合はcauseをコンストラクタへ渡してください。 // レビューコメント例 例外メッセージにメールアドレスが含まれています。 ログへ出るため、顧客IDなど必要最小限の識別子へ変更してください。
例外をラップする場合は、原因となった例外を捨てないことも重要です。
try {
paymentClient.authorize(request);
} catch (SocketTimeoutException ex) {
throw new ExternalApiTimeoutException(
"Payment API timed out. orderId=" + orderId, ex);
}
提出前のセルフチェック
- 例外名だけで、成立しなかった条件を説明できるか
- 業務ルール違反と技術的な失敗を混ぜていないか
- 呼び出し側で別対応しない例外を細分化しすぎていないか
- 元例外をcauseとして保持しているか
- メッセージに調査用IDがあり、機密情報がないか
- 既存の例外体系と単語が揃っているか
読みやすい例外処理を学ぶ参考書
例外名だけでなく、意図が伝わる名前や短い処理単位を身につけると、エラー処理全体も読みやすくなります。次の書籍は、レビューで説明できるコードを書く基礎固めに向いています。
リーダブルコード
コードレビューの「読みづらい」を減らす、最初の一冊。
命名、コメント、条件分岐、変数、メソッド分割を短い実例から学べます。Java専用書ではありませんが、現場で使える読みやすさの判断基準が身につきます。
- レビュー指摘の理由と直し方を整理したい
- 動くだけでなく、読みやすいコードを書きたい
当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。
この記事とあわせて読みたい
まとめ
Javaの例外名は、処理を実行した場所ではなく、成立しなかった条件を表すと読みやすくなります。OrderExceptionのような大きな名前へ集約せず、在庫不足、状態不正、外部APIのタイムアウトなど、呼び出し側が判断できる粒度にします。
一方で、例外クラスを細かく増やすこと自体が目的ではありません。利用者への応答、ログ、リトライ、トランザクションなど、後続処理が変わるかを基準にし、プロジェクトの既存ルールへ揃えましょう。
