SJ blog
Java
Z

信頼度ランク

S 公式ソース確認済み
A 成功実績多数・失敗例少数
B 賛否両論
C 動作未確認・セキュリティリスク高
Z 個人所感

Javaの例外処理ベストプラクティス完全版

try-catchの書き方、検査例外と非検査例外の使い分け、カスタム例外の作り方、try-with-resourcesまで解説します。

一言結論

例外はログして再スローするか、完全に処理するかのどちらかにすべきで、catchして握りつぶす(何もしない)のが最悪のパターンであり障害調査を著しく困難にする。

例外の種類

Throwable
├── Error(プログラムで対処しない)
│   ├── OutOfMemoryError
│   └── StackOverflowError
└── Exception
    ├── RuntimeException(非検査例外)
    │   ├── NullPointerException
    │   ├── IllegalArgumentException
    │   └── IndexOutOfBoundsException
    └── IOException(検査例外)
        ├── FileNotFoundException
        └── ...

検査例外(Checked Exception)

コンパイラが catch または throws 宣言を強制する例外。IOExceptionSQLException など。

非検査例外(Unchecked Exception)

RuntimeException のサブクラス。プログラミングミスを示すものが多い。

基本的な try-catch

try {
    String text = readFile("data.txt");
    process(text);
} catch (FileNotFoundException e) {
    System.err.println("ファイルが見つかりません: " + e.getMessage());
} catch (IOException e) {
    System.err.println("読み込みエラー: " + e.getMessage());
} finally {
    // 必ず実行される(リソース解放など)
    cleanup();
}

try-with-resources(Java 7以降)

Closeable/AutoCloseable を実装したリソースは自動でクローズされます。

// 旧来の書き方(finally でクローズ)
BufferedReader reader = null;
try {
    reader = new BufferedReader(new FileReader("data.txt"));
    String line = reader.readLine();
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (reader != null) {
        try { reader.close(); } catch (IOException e) { }
    }
}

// try-with-resources(推奨)
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
    String line = reader.readLine();
} catch (IOException e) {
    throw new AppException("ファイル読み込みに失敗しました", e);
}

Multi-catch(Java 7以降)

try {
    // ...
} catch (FileNotFoundException | DatabaseException e) {
    log.error("データ取得エラー: {}", e.getMessage());
}

カスタム例外の作り方

// アプリケーション固有の基底例外
public class AppException extends RuntimeException {
    private final ErrorCode errorCode;

    public AppException(String message, ErrorCode errorCode) {
        super(message);
        this.errorCode = errorCode;
    }

    public AppException(String message, ErrorCode errorCode, Throwable cause) {
        super(message, cause);
        this.errorCode = errorCode;
    }

    public ErrorCode getErrorCode() {
        return errorCode;
    }
}

public enum ErrorCode {
    USER_NOT_FOUND,
    INVALID_INPUT,
    SYSTEM_ERROR
}
// 使用例
public User findUser(int id) {
    User user = repository.find(id);
    if (user == null) {
        throw new AppException("ユーザーが見つかりません: " + id, ErrorCode.USER_NOT_FOUND);
    }
    return user;
}

ベストプラクティス

1. 例外をスワローしない

// NG: 例外を無視する
try {
    process();
} catch (Exception e) {
    // 何もしない(絶対にやってはいけない)
}

// OK: 少なくともログを出す
try {
    process();
} catch (Exception e) {
    log.error("処理中にエラーが発生しました", e);
    throw e;
}

2. 原因例外を連鎖させる

// NG: 原因を失う
try {
    repository.save(entity);
} catch (SQLException e) {
    throw new AppException("保存に失敗しました");  // 原因が消える
}

// OK: 原因をラップする
try {
    repository.save(entity);
} catch (SQLException e) {
    throw new AppException("保存に失敗しました", e);  // e を渡す
}

3. 具体的な例外をキャッチする

// NG: 広すぎる
catch (Exception e) { ... }

// OK: 具体的に
catch (FileNotFoundException e) { ... }
catch (IOException e) { ... }

4. 例外でフロー制御をしない

// NG: 例外でループを終わらせる
try {
    for (int i = 0; ; i++) {
        process(array[i]);  // ArrayIndexOutOfBoundsException を期待
    }
} catch (ArrayIndexOutOfBoundsException e) {
    // ループ終了
}

// OK: 条件で制御
for (int i = 0; i < array.length; i++) {
    process(array[i]);
}

5. 引数の検証は IllegalArgumentException を使う

public void deposit(int amount) {
    if (amount <= 0) {
        throw new IllegalArgumentException("金額は正の値である必要があります: " + amount);
    }
    // ...
}

ログの書き方

// スタックトレースを含めてログ出力(SLF4J)
log.error("エラーが発生しました", e);  // OK: {} は不要

// NG: printStackTrace() の直接呼び出し(本番環境では非推奨)
e.printStackTrace();

まとめ

することしないこと
原因例外を連鎖させる例外を無視する(空のcatch)
具体的な例外をキャッチException で一括catch
try-with-resources を使うfinallyでクローズ忘れ
メッセージに具体情報を含める「エラーが発生しました」だけ