背景: 直近の仕事での反省から

先日、プロジェクトで例外処理に関する問題が発覚しました。私は、例外が発生した際にcatchブロックでログを出力するだけのコードを書いていたのですが、これが原因でエラーが適切に伝播せず、後続の処理に影響を与えてしまいました。

その時は「catchで例外を処理したからこれで良し」と思い込んでいたのですが、実際にはエラーを握り潰してしまっていたのです。この経験から、try-catchの正しい使い方を改めて整理する必要性を感じました。

この記事では、catchでエラーを潰さないための方法」と、「再スローの正しい使い方」について、Kotlinの機能を活用したベストプラクティスを交えてまとめます。


問題: catchでエラーを潰してしまうケース

例外処理でありがちなミスとして、「catchで例外をログに記録しただけで処理を終了してしまう」ことがあります。この場合、エラーの詳細が伝わらず、後続の処理に影響を与える可能性があります。

典型的な悪い例

try {
    // 例外が発生する可能性のある処理
} catch (e: Exception) {
    println("エラー発生: ${e.message}") // ログを出力するだけ
}

なぜこれが問題か

  1. エラーが握り潰される
    • エラーの詳細情報が呼び出し元に伝わらないため、デバッグが困難になります。
  2. 後続処理に悪影響
    • エラーが解消されないまま進行する可能性があります。
  3. エラーの根本原因が隠蔽される
    • スタックトレースが失われるため、問題の根本原因を特定するのが難しくなります。

解決策: 再スローを使う

エラーを呼び出し元に伝えるために、catch内で例外を再スローすることが重要です。

基本的な再スローの例

try {
    // 例外が発生する可能性のある処理
} catch (e: Exception) {
    println("エラー発生: ${e.message}") // ログを出力
    throw e // 再スローして呼び出し元に伝える
}

追加情報を付加したい場合

場合によっては、例外に付加情報を加えて再スローすることも有用です。

例: コンテキスト情報を追加

try {
    // 例外が発生する可能性のある処理
} catch (e: IOException) {
    throw RuntimeException("ファイル読み込み中にエラーが発生しました: ${e.message}", e)
}
  • 元の例外をラップすることで:
    • エラーの文脈が分かりやすくなる。
    • 元の例外情報(スタックトレースなど)も保持される。

Kotlinの機能を活用した例外処理のベストプラクティス

Kotlinには、例外処理をより安全かつ効率的に行うための機能がいくつかあります。以下にその例を示します。

1. runCatchingを使った簡潔な例外処理

KotlinのrunCatching関数を使うと、try-catchをより簡潔に記述できます。また、結果をResult型で返すため、エラーハンドリングが容易です。

val result = runCatching {
    // 例外が発生する可能性のある処理
}.onFailure { e ->
    println("エラー発生: ${e.message}") // ログを出力
}.getOrThrow() // エラーがあれば再スロー

2. Result型を活用したエラーハンドリング

runCatchingの戻り値であるResult型を使うことで、エラーハンドリングをより柔軟に行えます。

val result = runCatching {
    // 例外が発生する可能性のある処理
}

result.onSuccess { value ->
    println("成功: $value")
}.onFailure { e ->
    println("エラー発生: ${e.message}") // ログを出力
    throw e // 再スロー
}

3. カスタム例外クラスの作成

特定のエラータイプに対してカスタム例外クラスを作成することで、エラーの種類を明確にし、デバッグを容易にします。

class FileReadException(message: String, cause: Throwable) : RuntimeException(message, cause)

try {
    // ファイル操作の例
} catch (e: IOException) {
    throw FileReadException("ファイル読み込み中にエラーが発生しました", e)
}

catchを使うときの注意点

  1. エラーを握り潰さない
    • 必要に応じて例外を再スローすることで、エラーの詳細を呼び出し元に伝えます。
  2. エラーに付加情報を加える
    • 再スロー時に文脈情報を追加することで、後続のデバッグが容易になります。
  3. ログと再スローを組み合わせる
    • エラーを記録しつつ、再スローして正確なエラーの伝播を担保します。
  4. Kotlinの機能を活用する
    • runCatchingResult型を使うことで、例外処理を簡潔かつ安全に行えます。

良い例

以下は、try-catchを適切に使った例です。

1. 再スローによるエラー伝播

try {
    // 例外が発生する可能性のある処理
} catch (e: Exception) {
    println("エラー発生: ${e.message}") // ログを出力
    throw e // 再スロー
}

2. 付加情報付きの再スロー

try {
    // ファイル操作の例
} catch (e: IOException) {
    throw RuntimeException("ファイル操作中にエラー: ${e.message}", e)
}

3. runCatchingを使った簡潔な例外処理

val result = runCatching {
    // 例外が発生する可能性のある処理
}.onFailure { e ->
    println("エラー発生: ${e.message}") // ログを出力
}.getOrThrow() // エラーがあれば再スロー

まとめ

  • ログを出力するだけでは不十分!
    • 必要に応じて例外を再スローし、呼び出し元で適切に処理する。
  • 例外に付加情報を加えることでデバッグ効率アップ!
    • 例外の文脈を追加することで、エラーの発生箇所や原因が明確になります。
  • Kotlinの機能を活用して例外処理を簡潔に!
    • runCatchingResult型を使うことで、安全かつ効率的な例外処理が可能です。

この経験を通じて、例外処理の重要性を再認識しました。同様のミスを防ぐために、この記事が少しでも役立てば幸いです!