KotlinのResult型について

採用はこちら

KotlinのResult<T>は、処理の成功または失敗を1つの値として表現するための型です。

  • 成功した場合は T 型の値を持つ
  • 失敗した場合は Throwable を持つ

つまり、Result<T>を使うと、「この関数は成功するかもしれないし、失敗するかもしれない」ということを戻り値の型で表せます。

たとえば、文字列を数値に変換する処理や、ファイル読み込み、API呼び出しのように、失敗が起こりうる処理と相性がよいです。

目次

なぜResult型を使うのか

Kotlinで失敗を扱う方法としては、主に次の2つがあります。

例外を投げる

fun parseInt(text: String): Int {
    return text.toInt()
}

この場合、失敗すると例外が飛びます。

ただし、関数のシグネチャだけを見ると、呼び出し側は失敗の可能性を見落としやすいです。

null を返す

fun parseIntOrNull(text: String): Int? {
    return text.toIntOrNull()
}

これは扱いやすい一方で、なぜ失敗したのかという情報は失われます。

Result を返す

fun parseInt(text: String): Result<Int>

この形なら、「成功なら Int、失敗なら例外情報を含む」ということを明示できます。

基本的な作り方

Resultは、runCatching を使って作ることが多いです。

val result: Result<Int> = runCatching {
    "123".toInt()
}

これはブロックを実行して、

  • 正常終了したら Result.success(...)
  • 例外が発生したら Result.failure(...)

として返します。

失敗例はこうです。

val result: Result<Int> = runCatching {
    "abc".toInt()
}

この場合、NumberFormatException がその場で外に投げられるのではなく、Result の失敗側に格納されます。

成功か失敗かを調べる

Resultには、成功・失敗を確認するためのプロパティがあります。

if (result.isSuccess) {
    println("成功")
}

if (result.isFailure) {
    println("失敗")
}

ただし実際には、単純に判定するよりも、後述する getOrElsefold を使ったほうが読みやすいことが多いです。

値の取り出し方

getOrNull()

成功なら値、失敗なら null を返します。

val value: Int? = result.getOrNull()

getOrDefault(defaultValue)

失敗した場合に固定のデフォルト値を返します。

val value = result.getOrDefault(0)

getOrElse { ... }

失敗時に、例外に応じて代替値を返せます。

val value = result.getOrElse { exception ->
    0
}

getOrThrow()

成功なら値を返し、失敗なら中に保持している例外を再スローします。

val value = result.getOrThrow()

これは、途中ではResultとして扱っていても、最終的には例外として表に出したい場面で使えます。

成功時・失敗時に副作用を挟む

onSuccess

成功時にだけ処理を実行します。

result.onSuccess {
    println("成功値: $it")
}

onFailure

失敗時にだけ処理を実行します。

result.onFailure {
    println("失敗: ${it.message}")
}

この2つは、値を変換するためというより、副作用のために使うものです。

たとえばログ出力、トラッキング、デバッグなどに向いています。

成功時の値を変換する

map

成功時の値を変換します。

失敗している場合は、そのまま失敗が維持されます。

val result = runCatching { "123".toInt() }

val doubled: Result<Int> = result.map { it * 2 }

この例では、成功していれば 246 になります。

ただし重要なのは、map の transform の中で例外が起きた場合、その例外は Result.failure に包まれず、再スローされるという点です。

val result = runCatching { "123" }

val mapped = result.map {
    it.toInt()   // ここで例外が起きれば外に投げられる
}

mapCatching

こちらも成功時の値を変換しますが、transform内で起きた例外も Result.failure に包みます

val result = runCatching { "123" }

val parsed = result.mapCatching {
    it.toInt()
}

使い分けの目安

  • 変換処理が安全で、例外を起こさない前提なら map
  • 変換処理の中でも例外が起きうるなら mapCatching

この違いはかなり大事です。

失敗から回復する

recover

失敗している場合に、代替値を返して成功に変えます。

val result = runCatching { "abc".toInt() }
    .recover { 0 }

この結果は、失敗ではなく成功値 0 を持つ Result<Int> になります。

ただし、recover の処理内で例外が起きた場合、その例外は再スローされます。

recoverCatching

こちらは、回復処理の中で起きた例外も Result.failure に包みます。

val result = runCatching { "abc".toInt() }
    .recoverCatching { e ->
        if (e is NumberFormatException) 0 else throw e
    }

使い分けの目安

  • 回復処理で例外が起きない前提なら recover
  • 回復処理内の例外も吸収して Result に包みたいなら recoverCatching

最終的に値へ畳み込む fold

Result を最終的な1つの値に変換したいときに便利なのが fold です。

val message = result.fold(
    onSuccess = { "成功: $it" },
    onFailure = { "失敗: ${it.message}" }
)

fold のよいところは、成功時と失敗時の両方の処理を書くことが前提になっていることです。

そのため、分岐漏れが起きにくくなります。

表示文言を作るときや、UI層で最終的な出し分けをしたいときに便利です。

典型的な使用例

文字列を安全に数値変換する

fun parseInt(text: String): Result<Int> =
    runCatching { text.toInt() }

使う側

parseInt("123")
    .onSuccess { println("数値: $it") }
    .onFailure { println("変換失敗: ${it.message}") }

成功時だけ加工する

fun fetchUserName(): Result<String> =
    runCatching {
        "yamada"
    }.map {
        it.uppercase()
    }

成功していれば大文字化され、失敗していればそのまま失敗が伝わります。

失敗時にフォールバックする

fun loadPort(): Int {
    return runCatching {
        System.getenv("PORT").toInt()
    }.getOrElse {
        8080
    }
}

設定値読み込みのように、「失敗したら安全な既定値に落とす」という用途で使いやすいです。

Result型のメリット

失敗可能性を型で表現できる

fun parseInt(text: String): Result<Int>

このシグネチャを見るだけで、「失敗する可能性がある関数だ」と分かります。

成功・失敗を値として扱える

try-catch は制御構文ですが、Result は値です。

そのため、

  • 変数に入れられる
  • 関数から返せる
  • mapfold でつなげられる

という利点があります。

失敗理由を保持できる

null と違って、失敗理由として Throwable を持てます。

そのため、原因の追跡がしやすくなります。

注意点

runCatchingThrowable を広く捕まえる

これは便利ですが、捕まえすぎることがあります。

特にコルーチンでは、CancellationException まで包んでしまう可能性があります。

キャンセルは通常のエラーとは扱いが違うため、むやみに runCatching で包むと不自然な挙動になることがあります。

たとえば、コルーチン内で使うなら、必要に応じてキャンセル例外を再スローする設計を考えたほうが安全です。

suspend fun fetchDataSafely(): Result<String> {
    return try {
        Result.success(fetchData())
    } catch (e: CancellationException) {
        throw e
    } catch (e: Throwable) {
        Result.failure(e)
    }
}

何でもResultにするのが正解ではない

Result は便利ですが、あらゆる失敗をこれで表せばよいわけではありません。

たとえば、

  • 明らかなバグ
  • 前提条件違反
  • 本来起きるべきでない異常

まで全部 Result に包むと、障害が埋もれてしまうことがあります。

業務エラーの表現には向かない場合がある

Result の失敗側は Throwable だけです。

そのため、業務上の失敗を細かく型で表したい場合には、少し表現力が足りないことがあります。

たとえば、

  • 入力不正
  • 在庫切れ
  • 権限不足
  • 認証失敗

のように、失敗の種類そのものが重要な場合は、独自の型を作るほうが分かりやすいことがあります。

Resultsealed class の使い分け

Result が向いている場面

  • 単純な成功 / 失敗を扱いたい
  • 例外を値として返したい
  • 変換処理をチェーンしたい
  • インフラ層やユーティリティ層で使いたい

sealed class が向いている場面

  • 失敗の種類を型で明確に分けたい
  • 業務ルールに基づく失敗を表現したい
  • when で網羅的に分岐したい

sealed interface LoginResult {
    data class Success(val userId: String) : LoginResult
    data object InvalidPassword : LoginResult
    data object UserNotFound : LoginResult
    data object LockedAccount : LoginResult
}

このように、ドメイン上の意味をきちんと持たせたいなら、sealed classsealed interface のほうが向いていることがあります。

try-catchResult の違い

これは非常に重要です。

try-catch

  • 例外処理のための構文
  • その場でエラーハンドリングするのに向いている

Result

  • 成功 / 失敗を表す値
  • 後続処理へ渡したり、関数から返したりしやすい

たとえば、try-catch は次のように書きます。

val value = try {
    "123".toInt()
} catch (e: Throwable) {
    0
}

一方、Result ならこうです。

val value = runCatching { "123".toInt() }
    .getOrElse { 0 }

このように、Result は「エラー処理を値としてつなぐ」書き方に向いています。

実務でのおすすめの考え方

実務では、次のように考えると整理しやすいです。

  • 通常起こりうる失敗
    例: 入力変換失敗、通信失敗、ファイル読み込み失敗
    Result が向く
  • 本来起こるべきでない異常
    例: バグ、破壊された状態、前提条件違反
    → 例外として扱うほうが自然
  • 業務的な失敗分類が重要
    例: 認証失敗、在庫切れ、権限不足
    → 独自の sealed class を検討

最初に覚えるべきメソッド

最初はこのあたりを押さえると十分です。

  • runCatching
  • getOrNull
  • getOrElse
  • getOrThrow
  • map
  • mapCatching
  • recover
  • fold

さらに慣れてきたら、

  • recoverCatching
  • onSuccess
  • onFailure

まで理解すると、かなり使いこなしやすくなります。

まとめ

KotlinのResult型は、成功と失敗を1つの値で扱うための型です。

特に、失敗が起こりうる処理を戻り値として明示したいときに便利です。

ただし、使うときは次の点を意識すると失敗しにくいです。

  • runCatchingThrowable を広く捕まえる
  • map / recover は変換処理内の例外を再スローする
  • mapCatching / recoverCatching は変換処理内の例外も Result.failure に包む
  • 業務上の失敗分類が重要なら sealed class のほうが向いていることもある

ひとことで言うと、Result「例外をその場で処理する」のではなく、「成功か失敗かを値として持ち回る」ための仕組みと理解すると分かりやすいです。

以上、KotlinのResult型についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次