Java 逆引き集 | カスタム例外の作り方 — 明確なエラー伝搬

Java Java
スポンサーリンク

カスタム例外の作り方 — 明確なエラー伝搬

「何が問題か」を正確に伝えるのが例外の役割。標準例外だけでは業務の意図が伝わりにくい場面で、カスタム例外が活躍します。APIレイヤでは、呼び出し側が“何をどう処理すべきか”判断しやすい形でエラーを返す設計が大切です。


目的と使いどころ

  • 意味の明確化: 「在庫不足」「権限なし」「入力不正」など、業務語で伝える。
  • 捕捉の簡略化: 例外型ごとに対応を分岐(再試行、400/403/404/409などへのマッピング)。
  • 診断容易化: IDや状態など、原因追跡に必要な情報を例外に持たせる。

checked と unchecked の選び分け

  • checked(Exception継承): 呼び出し側に“対処義務”を課したい。外部要因(IO、ネット、DB)や回復可能性があるとき。
  • unchecked(RuntimeException継承): プログラムのバグや前提違反。呼び出し側に明示のtry/catchを強要しない。
  • 指針:
    • 外部障害は checked。
    • 契約違反・ドメイン整合性違反は unchecked。
    • フレームワーク/ライブラリ境界での再マッピングも有効(内部はuncheckedで高速に伝搬、API出口でHTTPへ変換)。

基本的な定義パターン

最小構成(メッセージのみ)

// checked例外
public class DataNotFoundException extends Exception {
    public DataNotFoundException(String message) { super(message); }
}

// unchecked例外
public class InvalidOrderStateException extends RuntimeException {
    public InvalidOrderStateException(String message) { super(message); }
}
Java

原因例外(cause)を伝搬

public class ExternalServiceException extends Exception {
    public ExternalServiceException(String message, Throwable cause) {
        super(message, cause);
    }
}
Java

追加フィールドでコンテキストを保持

public class StockShortageException extends RuntimeException {
    private final String productId;
    private final int requested;
    private final int available;

    public StockShortageException(String productId, int requested, int available) {
        super("在庫不足: productId=" + productId + ", requested=" + requested + ", available=" + available);
        this.productId = productId;
        this.requested = requested;
        this.available = available;
    }

    public String getProductId() { return productId; }
    public int getRequested() { return requested; }
    public int getAvailable() { return available; }
}
Java

throw / throws と伝搬のテンプレ

例外を投げる(throw)

void validateAge(int age) {
    if (age < 0) throw new IllegalArgumentException("年齢は0以上");
}
Java

呼び出し側に委ねる(throws)

// checked例外を宣言して、呼び出し側に処理を委ねる
public Order fetchOrder(String id) throws DataNotFoundException {
    // 見つからなければ投げる
    throw new DataNotFoundException("注文が見つかりません: id=" + id);
}
Java

トランスレート(別の意味へ変換)

public Customer find(String id) {
    try {
        return repository.load(id);
    } catch (SQLException e) {
        throw new ExternalServiceException("DBエラー", e); // 意味付けして再スロー
    }
}
Java

API 層での捕捉とマッピング

コントローラで型ごとにHTTPへ変換

public Response getOrder(String id) {
    try {
        Order o = service.getOrder(id);
        return Response.ok(o);
    } catch (DataNotFoundException e) {
        return Response.status(404).body(e.getMessage());
    } catch (InvalidOrderStateException e) {
        return Response.status(409).body(e.getMessage());
    } catch (ExternalServiceException e) {
        return Response.status(502).body("上流障害: " + e.getMessage());
    }
}
Java
  • ラベル化: 型名でエラー意図を表現し、catchの分岐を読みやすく。
  • メッセージ方針: ユーザー表示用とログ用を分ける設計(必要ならコードや詳細フィールド)。

例題で身につける

例題1: 在庫不足を明確化(unchecked)

class OrderService {
    void place(String productId, int qty) {
        int available = checkStock(productId);
        if (qty > available) {
            throw new StockShortageException(productId, qty, available);
        }
        // 正常処理…
    }

    int checkStock(String productId) { return 3; }
}
Java
  • ポイント: ドメイン違反はRuntimeExceptionで高速伝搬。例外が保持する追加情報で、上位でのメッセージ生成や監視を簡単に。

例題2: 外部連携失敗の委ね方(checked)

class PaymentService {
    void pay(String orderId) throws ExternalServiceException {
        try {
            callGateway(orderId);
        } catch (IOException e) {
            throw new ExternalServiceException("決済ゲートウェイ通信失敗: orderId=" + orderId, e);
        }
    }

    void callGateway(String orderId) throws IOException { /* ... */ }
}
Java
  • ポイント: 外部要因はcheckedで呼び出し側に再試行/フォールバック判断を委ねる。

例題3: API層で一元ハンドリング

class ApiController {
    OrderService orderService = new OrderService();
    PaymentService paymentService = new PaymentService();

    Response handleCheckout(String productId, int qty, String orderId) {
        try {
            orderService.place(productId, qty);
            paymentService.pay(orderId);
            return Response.ok("完了");
        } catch (StockShortageException e) {
            return Response.status(409).body("在庫不足: " + e.getAvailable() + "個のみ");
        } catch (ExternalServiceException e) {
            return Response.status(502).body("外部連携失敗。後で再試行してください");
        } catch (Exception e) {
            return Response.status(500).body("不明なエラー");
        }
    }
}
Java
  • ポイント: 型別マッピングで、呼び出し側(クライアント)にとって分かりやすい反応を返す。

すぐ使えるテンプレ

  • 最小限のカスタム例外
public class DomainException extends RuntimeException {
    public DomainException(String message) { super(message); }
    public DomainException(String message, Throwable cause) { super(message, cause); }
}
Java
  • IDとコード付き(API設計向け)
public class ApiErrorException extends RuntimeException {
    private final String code;   // "ORDER_NOT_FOUND" など
    private final String id;     // 関連ID

    public ApiErrorException(String code, String message, String id) {
        super(message);
        this.code = code; this.id = id;
    }
    public String getCode() { return code; }
    public String getId() { return id; }
}
Java
  • ユーティリティで再スロー統一
public final class Exceptions {
    private Exceptions() {}
    public static <T> T wrapSql(SqlSupplier<T> s) {
        try { return s.get(); }
        catch (SQLException e) { throw new ExternalServiceException("DB障害", e); }
    }
    @FunctionalInterface interface SqlSupplier<T> { T get() throws SQLException; }
}
Java

ありがちな落とし穴と対処

  • 抽象的すぎる例外名:
    • 対処: 具体名で意図を示す。 DataNotFoundException / InvalidStatusException など。
  • 階層が過剰に深い:
    • 対処: 2階層程度に抑える。 基底(ドメイン/外部連携)+具体型。
  • causeを捨てる:
    • 対処: 必ず原因を保持。 コンストラクタでcauseを受けてsuperへ。
  • ユーザー向けとログ向けの混同:
    • 対処: 表示文言と技術詳細を分離。 コード+人間向けメッセージ+詳細はログ。
  • checkedの乱用でtry/catchだらけ:
    • 対処: 境界でまとめて処理。 ドメイン内はunchecked、インフラ境界でchecked→ドメイン例外へ変換。

まとめ

  • 意図の可視化: 例外名と型で“何が起きたか”を即伝達。
  • 伝搬設計: checked/uncheckedを使い分け、API出口で意味あるレスポンスへ。
  • 追跡容易: IDや状態を例外に持たせ、原因解析を短時間で。

タイトルとURLをコピーしました