概要
Kotlin の例外処理は、Java の仕組みを継承しつつも、より安全で柔軟です。
特に、try-catch 構文だけでなく、runCatching や Result、コルーチン専用の CoroutineExceptionHandler など、モダンな設計パターンが利用できます。
この記事では以下の観点から体系的に紹介します:
-
try-catch-finallyの基本構文 -
throw/Throwable/Exceptionの階層 - Kotlin独自の
runCatching/ResultAPI - コルーチンの例外処理 (
SupervisorJob,CoroutineExceptionHandler) - 実戦設計パターン(安全なエラーハンドリング構造)
1. 基本構文:try-catch-finally
Kotlin では、例外処理は次のように書けます:
fun divide(a: Int, b: Int): Int {
return try {
a / b
} catch (e: ArithmeticException) {
println("Error: ${e.message}")
0 // 代替値を返す
} finally {
println("処理終了")
}
}
fun main() {
println(divide(10, 2)) // => 5
println(divide(10, 0)) // => 0
}
ポイント:
-
tryは式(expression)として値を返せる -
catchで例外を補足し、エラーメッセージなどを取得可能 -
finallyは例外発生に関わらず必ず実行
2. 例外階層:Throwable とその派生
Kotlin(およびJVM)では、例外は Throwable クラスを基底に持ちます。
| クラス | 意味 | 例 |
|---|---|---|
Error |
システム的な致命的エラー | OutOfMemoryError |
Exception |
通常の実行時例外 |
IOException, RuntimeException
|
RuntimeException |
実行中のロジックエラー | NullPointerException |
Checked Exception |
Javaのみ。Kotlinでは存在しない | — |
Kotlin には Checked Exception(例外宣言強制) が存在しません。
→ より柔軟に例外を扱えるが、自分で責任を持ってハンドリングする必要があります。
3. runCatching と Result による安全な処理
Kotlin では、例外を値として扱える runCatching ブロックが推奨されています。
fun safeDivide(a: Int, b: Int): Result<Int> =
runCatching { a / b }
fun main() {
val result = safeDivide(10, 0)
result.onSuccess { println("結果: $it") }
.onFailure { println("失敗: ${it.message}") }
}
Result のチェーン利用
val data = runCatching { "100".toInt() }
.map { it * 2 }
.recover { 0 }
.getOrThrow()
println(data) // => 200
| メソッド | 意味 |
|---|---|
map |
成功時の値を変換 |
recover |
失敗時に代替値を返す |
getOrThrow |
成功なら値、失敗なら例外再スロー |
getOrNull |
成功なら値、失敗なら null
|
特徴:
- 例外を明示的に扱わずに、安全なチェーンが書ける
- 関数型スタイルで扱いやすい(エラーハンドリングも表現的)
4. コルーチンでの例外処理
コルーチンでは例外が非同期的に発生するため、特別な仕組みが必要です。
try-catch は有効だが、launchとasyncで挙動が異なる
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
try {
throw RuntimeException("Error in launch")
} catch (e: Exception) {
println("Caught in launch: ${e.message}")
}
}
job.join()
}
launch は例外を自動的に上位スコープへ伝播しますが、
async は結果を待つ (await()) まで例外が保留されます。
val deferred = async {
throw RuntimeException("Error in async")
}
try {
deferred.await()
} catch (e: Exception) {
println("Caught in async: ${e.message}")
}
CoroutineExceptionHandler
グローバルに例外を処理したい場合は、CoroutineExceptionHandler を使います。
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught by handler: ${exception.message}")
}
fun main() = runBlocking {
val scope = CoroutineScope(SupervisorJob() + handler)
scope.launch {
throw RuntimeException("Something went wrong")
}
delay(100L)
}
ポイント:
-
CoroutineExceptionHandlerはlaunch系にのみ有効 -
asyncの場合はawait()時に手動で補足する必要あり -
SupervisorJobを組み合わせると他の子コルーチンへ伝播しない
5. 実戦設計パターン:安全なエラーハンドリング構造
例外処理を Clean Architecture 的に整理する場合:
suspend fun fetchUser(id: String): Result<User> =
runCatching {
api.getUser(id).also {
if (it.isBanned) throw IllegalStateException("User banned")
}
}
fun handleUserResult(result: Result<User>) {
result.fold(
onSuccess = { println("User: ${it.name}") },
onFailure = { println("Error: ${it.message}") }
)
}
メリット
- 例外を「値」として上層に返せる(副作用を分離)
-
runCatchingで try/catch 構文を簡潔化 - コルーチンとも自然に統合できる
まとめ
| 構文 / API | 特徴 |
|---|---|
try-catch-finally |
基本構文。副作用処理にも使用可 |
throw / Throwable
|
例外階層の基本構造 |
runCatching |
例外を安全に値として扱う |
Result |
成功 / 失敗を表すデータ型 |
CoroutineExceptionHandler |
非同期例外のグローバル処理 |
SupervisorJob |
子コルーチン間の独立性を確保 |