Article
🔍 Javaのtry-with-resources:本当の原因を隠さないsuppressed例外の読み方
ファイルやDB接続を使うJavaの処理では、try-with-resourcesを使うと終了時にclose()を呼び出せます。
ただし、本来の処理も、後片付けのclose()も失敗した場合、どちらの例外が残るのでしょうか?
障害調査で「接続を閉じるときのエラーだけを見て、SQL実行時の失敗を見落とす」といった混乱を防ぐため、Javaのsuppressed exception(抑制された例外)を実行例で確認します。
対象:Java 17以降で動くサンプル。言語仕様は2026年10月26日時点のJava SE 27の公式仕様で確認し、サンプルはOpenJDK 21で
--release 17を指定してコンパイル・実行しました。
1. try-with-resourcesは何をしている?
リソースとは、使用後に解放が必要なファイル、ストリーム、DB接続などのことです。
AutoCloseableというインターフェース(クラスが実装する機能の契約)のclose()を持つオブジェクトをtry (...)に記述すると、処理が正常終了しても例外終了しても、後片付けのためにclose()が呼ばれます。
try (var input = java.nio.file.Files.newInputStream(path)) {
// ファイルを読み込む
} // ここを抜けるとinput.close()が呼ばれる
pathはjava.nio.file.Path型の変数を想定した抜粋です。
ただし、「close()を試みる」ことと「解放が必ず成功する」ことは別です。close()自身が例外を投げる場合もあります。
2. 本体とclose()が両方失敗するとどうなる?
そのまま動かせる例をSuppressedExceptionDemo.javaとして保存します。
import java.io.IOException;
public class SuppressedExceptionDemo {
static final class Resource implements AutoCloseable {
private final String name;
Resource(String name) {
this.name = name;
}
@Override
public void close() throws IOException {
System.out.println("close: " + name);
throw new IOException("close " + name + " failed");
}
}
public static void main(String[] args) {
try (Resource first = new Resource("first");
Resource second = new Resource("second")) {
throw new IllegalStateException("operation failed");
} catch (Exception e) {
System.out.println("primary: " + e.getMessage());
for (Throwable suppressed : e.getSuppressed()) {
System.out.println("suppressed: " + suppressed.getMessage());
}
}
}
}
ターミナルで次の2コマンドを実行します。
javac --release 17 SuppressedExceptionDemo.java
java SuppressedExceptionDemo
javac:Javaのソースをコンパイル(実行できるクラスファイルへ変換)するコマンド。--release 17:Java 17向けの言語・API・クラスファイルとしてコンパイルするオプション。JDK 17以上のjavacが必要です。java SuppressedExceptionDemo:生成したクラスのmainメソッドを実行するコマンド。
実行結果:
close: second
close: first
primary: operation failed
suppressed: close second failed
suppressed: close first failed
ここで確認できるのは、次の3点です。
- 複数のリソースは、宣言と逆順に
close()される(second→first)。 tryの本体で発生したIllegalStateExceptionが主例外(外側へ通知される中心の例外)になる。- 後片付け時の2つの
IOExceptionは消えず、主例外のgetSuppressed()から取得できる。
**抑制された例外(suppressed exception)**は「無視されるエラー」ではありません。主例外を上書きせずに、後から診断できるよう保存された例外です。
3. 本体が成功した場合は?
本体のthrow new IllegalStateException(...)を、例えばSystem.out.println("operation ok");に置き換えてみてください。
この場合、先に失敗するsecond.close()のIOExceptionが主例外になります。後から失敗するfirst.close()のIOExceptionは、その主例外のgetSuppressed()に入ります。
つまり、常に本体側の例外が主例外になるのではなく、先に発生した失敗の有無で決まる、ということです。
Java言語仕様では、複数リソースのtry-with-resourcesは入れ子の処理に相当し、後から宣言したリソースから閉じると定義されています。
4. getCause()とgetSuppressed()を混同しない
Javaの例外には、似ている2つの参照があります。
| API | 意味 | 代表例 |
|---|---|---|
getCause() |
この例外の原因となった別の例外 | new IllegalStateException("保存失敗", sqlException) |
getSuppressed() |
主例外を優先したため、外側へ直接投げなかった別の例外 | try本体の失敗に続くclose()の失敗 |
例えば業務例外に変換するときは、元の例外を原因(cause)として保持します。
try {
saveOrder();
} catch (java.sql.SQLException e) {
throw new IllegalStateException("注文の保存に失敗しました", e);
}
saveOrder()はSQLExceptionを投げ得る処理を表す抜粋です。こうしておけば、元の例外のスタックトレースやsuppressed情報をgetCause()経由でたどれます。
反対に、new IllegalStateException(e.getMessage())だけで包むと、原因の例外オブジェクトを失います。
5. JDBCでの使いどころ
JDBCは、JavaからリレーショナルDBにアクセスするための標準APIです。Connection、PreparedStatement、ResultSetなどのリソースを扱います。
以下は、DataSource dataSource(接続を取得する仕組み)とlong userIdが利用可能なメソッド内の抜粋です。
String sql = "SELECT name FROM users WHERE id = ?";
try (java.sql.Connection connection = dataSource.getConnection();
java.sql.PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setLong(1, userId);
try (java.sql.ResultSet rows = statement.executeQuery()) {
if (rows.next()) {
System.out.println(rows.getString("name"));
}
}
} catch (java.sql.SQLException e) {
// ロガーに例外オブジェクトを渡すと、原因やsuppressedの追跡に役立つ
e.printStackTrace(); // 説明用。本番では利用中のロガーを使う
throw new IllegalStateException("ユーザー取得に失敗しました", e);
}
このコードは自前で接続を取得・所有するケースの例です。Springの@TransactionalやJdbcTemplateなど、フレームワークが接続・トランザクションを管理する場合には、そちらの管理方法に従ってください。むやみに接続を手動で閉じる実装へ置き換えないことが重要です。
6. 障害調査とテストでのハマりどころ
- close()側のログだけで原因を断定しない:主例外の種類、
getCause()、getSuppressed()を確認します。printStackTrace()は通常、抑制された例外も表示します。 - 例外メッセージだけを再throwしない:元の
Throwableをcauseに渡して保持します。 - close()が必ず成功すると思わない:自動で呼ばれても、I/O障害などでクローズは失敗し得ます。
- 手書きのfinallyへ安易に戻さない:
finally中の例外で本体の例外が置き換わる書き方をすると、調査すべき失敗を見失います。 - テストでは2種類の失敗を分ける:「処理中に失敗+closeも失敗」と「処理は成功+closeで失敗」の両方を試すと、主例外の違いを検証できます。
まとめ
try-with-resourcesの利点は後片付けを短く書けることだけではありません。本体処理の失敗を主例外として残し、後片付けの失敗もsuppressedとして保持することで、障害の全体像を追いやすくなります。
本番ログでSuppressed:という行を見つけたら、無関係な情報として切り捨てず、主例外とセットで読むのが重要です。