Result Type — 成功(Success)または失敗(Failure)で完了する操作の結果を表すコンテナ型です。例外とは異なり、Resultはスタック巻き戻しなしでエラーを通常の値として渡すため、想定されるエラーの処理がより安全で合成可能になります。Apple Swift Documentation (2026)によると、SwiftのResult<Success, Failure>は、プログラムの実行を中断することなく、mapとflatMapによる自動エラー処理で操作を連鎖できます。
重要なポイント
Result Typeは、成功または失敗する可能性のある操作の結果をカプセル化するジェネリック型です。エラーが通常のフローを中断し、catchブロックを見つけるためにスタック巻き戻しが必要な例外とは異なり、Resultはエラーを通常の値として渡します。呼び出し側は常にオブジェクトを受け取り、その処理方法を決定します。これは特に想定されるエラー(無効な入力、ビジネスルール、サーバー拒否など)に有用で、例外では重すぎるメカニズムになります。
Resultの概念は関数型プログラミングに由来し、同様の型はEither(またはLeft/Right)と呼ばれます。SwiftではResultはSwift 5.0で標準型になりました。KotlinではResult<T>が標準ライブラリに登場し、Dart 3.0ではビルトインのResult<T>が導入されました。各実装はコンテナを操作するメソッドを提供します:map(成功値の変換)、flatMap(Result関数の連鎖)、mapError(エラーの変換)、fold(両方のケースを処理)。Result Typeはモバイル開発における関数型エラーハンドリングの要であり、想定されるシナリオに対するtry-catchの代替手段です。
Resultの主な利点は合成可能性です。それぞれが失敗する可能性のある複数の操作を、ネストしたtry-catchブロックなしで1つのチェーンに結合できます。チェーン内のいずれかの操作がFailureで失敗すると、チェーン全体が中断されFailureを返します — if条件やcatchブロックは一切不要です。これにより、特に複数のシーケンシャルなAPIリクエストやビジネスルールチェックのシナリオで、コードが直線的で読みやすくなります。
Swiftでは、Result<Success, Failure>は2つのケースを持つenumです:.success(Success)と.failure(Failure)。FailureはErrorプロトコルによって制約されます。SwiftのResultはmap、flatMap、mapError、getメソッドを持つ完全な関数型です。getは特別なメソッドで、結果が成功の場合はSuccess値を返し、Failureの場合はエラーをスローします。これにより、Resultは関数型スタイルと例外ベースのスタイルの間の橋渡しとして機能します。チェーン内でmap/flatMapで処理し、最後にdo-catch付きのgetを使用してthrowsコードと統合します。
SwiftのResultはジェネリックパラメータを持つenumであり、コンパイラはswitchやdo-catchによる網羅的な処理を強制できます。NetworkError enumに新しいケースを追加すると、そのケースが処理されていないすべてのswitch式でコンパイラがエラーを生成します。網羅的チェックは、例外に対するResultの主な利点です。コンパイラはビルド時にすべての可能なエラーが考慮されていることを保証します。対照的に、Swiftでは例外はコンパイラによってチェックされません(throwsのみが宣言され、エラー型は宣言されません)。
enum NetworkError: Error {
case badURL
case requestFailed(String)
case decodingFailed
}
func fetchUser(id: Int) -> Result<User, NetworkError> {
guard let url = URL(string: "https://api.example.com/users/\(id)") else {
return .failure(.badURL)
}
let result = performRequest(url: url)
switch result {
case let .success(data):
if let user = try? JSONDecoder().decode(User.self, from: data) {
return .success(user)
}
return .failure(.decodingFailed)
case let .failure(error):
return .failure(.requestFailed(error.localizedDescription))
}
}
// switchでの使用
let result = fetchUser(id: 42)
switch result {
case .success(let user):
showUser(user)
case .failure(.badURL):
logError("Invalid URL")
case .failure(.requestFailed(let msg)):
showAlert(msg)
case .failure(.decodingFailed):
logError("Decoding error")
}
Result<Success, Failure>は型レベルでエラーを型付けできます。fetchUser関数はResult<User, NetworkError>を返し、NetworkErrorは3つのケースを持つ具体的なenumです。呼び出し側は網羅的なカバレッジでswitchを使用して各ケースを処理します — コンパイラはすべてのバリアントが処理されたことを確認します。網羅的なswitchは例外に対するResultの重要な利点です。コンパイラは.badURL、.requestFailed、.decodingFailedの処理を忘れないことを保証します。例外の場合、コンパイラは処理を要求せず、コードレビューでcatch(.badURL)の欠落が見逃される可能性があります。
Kotlinでは、Result<T>は標準ライブラリのビルトイン型で、成功(T)またはエラー(Throwable)を表します。Swiftとは異なり、KotlinのResultは具体的なエラー型を指定できません — Throwableのみです。これは簡潔さのために行われましたが、when式による追加のエラー型チェックが必要です。KotlinのResultはfold(両方のケースを処理)、getOrNull(成功またはnull)、getOrDefault(成功またはデフォルト)、map、recover、andThen関数をサポートしています。Kotlinの特徴として、Resultは関数の境界を越えた直接的な伝搬を意図していません — 追加の適応なしにAndroid SDKメソッドやKotlin Coroutines APIの戻り値型として使用できません。
fun parseJson(input: String): Result<JsonObject> {
return runCatching {
JsonParser.parseString(input).asJsonObject()
}
}
fun validateEmail(email: String): Result<String> {
return if (email.contains("@")) {
Result.success(email.trim())
} else {
Result.failure(IllegalArgumentException("Invalid email"))
}
}
data class SignupData(val name: String, val email: String)
fun processSignup(name: String, email: String): SignupResult {
return validateEmail(email).fold(
onSuccess = { validateName(name) },
onFailure = { SignupResult.Error("Invalid email") }
)
}
runCatchingはKotlinの便利なラッパーで、任意の例外をキャッチしてResult.failureを返します。validateEmailは検証に応じてResult.successまたはResult.failureを返します。foldは両方のケースをコンパクトに処理します。Successの場合は次の検証関数が呼び出され、Failureの場合はSignupResult.Errorが返されます。重要:KotlinのResultはdata classフィールドへの格納やsuspend関数の境界を越えた直接的な受け渡しを意図していません — Jetpack ComposeやMVVMでUI状態を表現するには、カスタムのsealed class(Success/Error/Loading)を使用してください。
Dart 3.0以降、標準ライブラリにはビルトインのResult<T>が含まれています — 2つのコンストラクタを持つsealed classです:T.ok()(成功)とError.error()(ObjectとStackTraceを持つエラー)。Dart 3.0以前は、Flutter開発者はdartzパッケージのEither<L, R>やカスタムのsealed classを使用していました。DartのビルトインResultはミニマルで、map/flatMapを直接提供しません — これらの関数はwhenを使用するか、拡張メソッドを使って実装する必要があります。本格的な関数型処理には、fpdartのEitherがmap、flatMap、mapLeft、fold、andThen、bind演算子をサポートするより強力なソリューションです。
Either<L, R>はfpdartパッケージの左バイアス型で、Leftがエラー、Rightが成功です。ビルトインのResult<T>とは異なり、Eitherは型パラメータレベル(L)でエラーを型付けし、コンパイル時にエラー型を区別できます。fpdartパッケージは関数型コンビネータの完全なセットを提供します:map(Right -> Right)、mapLeft(Left -> Left)、flatMap(bind — ネストしたEither)、andThen(変換なしの連鎖)、fold(Eitherからの脱出)、getOrElse(デフォルト値)。関数型アプローチのFlutterアプリケーションでは、Eitherが事実上の標準です。
import 'dart:convert';
import 'package:fpdart/fpdart.dart';
class UserService {
Either<AppError, User> fetchUser(String id) {
try {
final response = await http.get(
Uri.parse('https://api.example.com/users/$id')
);
if (response.statusCode == 200) {
final user = User.fromJson(
json.decode(response.body)
);
return Either.of(user);
}
return Either.left(
AppError.serverError(response.statusCode)
);
} on SocketException catch (e) {
return Either.left(AppError.networkError(e.message));
}
}
}
// foldでの使用
final result = await service.fetchUser('42');
result.fold(
(left) => showError(left.message),
(right) => showUser(right),
);
Either<L, R>(fpdart)は左バイアス型です:Left — エラー、Right — 成功。fetchUserはEither<AppError, User>を返し、AppErrorは具体的なエラー型(serverError、networkError)を持つsealed classです。foldは両方のケースを処理します:最初のコールバックはLeft(エラー)用、2番目はRight(成功)用です。Dart 3.0ではビルトインのResultもfoldをサポートしていますが、map/flatMapは提供しません。チェーンには、fpdartのEitherがmap、flatMap(bind)、mapLeft、andThenを提供します — エラー合成のための関数型コンビネータの完全なセットです。
例外に対するResultの主な利点は合成です。それぞれが失敗する可能性のある複数の操作がある場合、ネストしたifやtry-catchなしでmapとflatMapを使って連鎖できます。mapは成功値を変換します:Result.success(x) -> Result.success(f(x))。flatMap(bindやandThenとも呼ばれる)は、変換自体がResultを返す場合に使用します:Result.success(x) -> f(x) -> Result<Y>。いずれかのステップがFailureで失敗すると、後続の操作は実行されません — チェーンは中断されます。
data class UserRequest(val userId: String, val token: String)
sealed class AuthError {
data object InvalidToken : AuthError()
data class UserNotFound(val id: String) : AuthError()
}
typealias Outcome<T> = Either<AuthError, T>
fun validateToken(token: String): Outcome<String> =
if (token.isNotBlank()) Either.right(token)
else Either.left(AuthError.InvalidToken)
fun fetchProfile(userId: String): Outcome<Profile> =
if (userId == "42") Either.right(Profile("Alice"))
else Either.left(AuthError.UserNotFound(userId))
// flatMapによる合成(fpdartのandThen)
val result = validateToken("abc123")
.flatMap { fetchProfile("42") }
.map { it.name }
.getOrElse { "Guest" }
println(result) // "Alice"
チェーン:validateToken -> fetchProfile -> map name -> getOrElse “ゲスト”。validateTokenがLeft(InvalidToken)を返すと、チェーンは中断され“ゲスト”を返します。fetchProfileがLeft(UserNotFound)を返す場合も同じく“ゲスト”です。両方の操作が成功すればプロフィール名が返されます。flatMapは、それぞれが失敗する可能性のあるEither関数を1つの線形チェーンに結合できます。従来の例外スタイルでは、同じコードに2つのネストしたtry-catchまたはnullチェックが必要です。最後のgetOrElseは合成の終了点で、Failureケースのデフォルト値を提供します。
Resultと例外は相互に排他的なアプローチではありません。それぞれに適用領域があり、適切に設計されたモバイルアプリケーションでは両方が使用されます。選択はエラーが想定内か想定外かによって異なります。Resultはビジネスロジックの一部である想定されたエラー用です:無効なメール、残高不足、レート制限超過など。例外は想定外のシステムエラー用です:ネットワーク喪失、OutOfMemoryError、NullPointerException(発生すべきではないが発生するもの)。
| 基準 | Result Type | 例外(Exception/Error) |
|---|---|---|
| エラーの種類 | 想定内(ビジネスロジック) | 想定外(システム) |
| パフォーマンス | 低コスト(スタック巻き戻しなし) | 高コスト(スタック巻き戻し、StackTrace取得) |
| 合成 | map/flatMapによる線形チェーン | ネストしたtry-catch — 読みにくい |
| コンパイラ | 網羅的チェック(switch/when) | Javaのchecked exceptionsのみ |
| 実行フロー | 中断されない — エラーは値として | 最も近いcatchまで中断される |
| テスト | 容易:結果を確認、isSuccess/isErrorをアサート | assertThrowsとモックオブジェクトが必要 |
| 使用すべき場合 | ビジネス検証、リクエストチェーン、フォーム | ネットワーク喪失、I/Oエラー、システムクラッシュ |
実用的なルール:エラーがアプリケーションの通常のワークフローの一部(ユーザーが無効なメールを入力、権限不足)の場合はResultを使用します。エラーが例外的な状況(サーバー応答なし、メモリ不足)の場合は例外を使用します。モバイル開発では、レイヤー境界でのResult(UseCase -> ViewModel)とレイヤー内部での例外(API -> Repository)は、両方のアプローチの利点を組み合わせた一般的なパターンです。
既存の例外ベースのコードからResultへの移行は段階的に行う必要があります。レイヤー境界から始めます。throws関数の呼び出しをResult { try ... }(Swift)やrunCatching { ... }(Kotlin)でラップします。次にRepositoryやUseCaseメソッドの戻り値の型をResult/Eitherに置き換え、内部実装は例外のままにします。最後のステップでViewModelを移行します。例外のあるUiStateの代わりに、sealed class UiState<T>(Loading、Success、Error)を使用し、ErrorはThrowableではなくドメインエラーを格納します。段階的な移行により、グローバルなリファクタリングなしで各レイヤーを個別にテストできます。
よくある質問
Optional(T?)は値の有無を表します — nilは“データなし”を意味しますが、理由は説明しません。Result(Success/Failure)は成功だけでなく、具体的な型を持つエラー理由も含みます。Optionalは不在が正常な場合(例:オプションのプロフィールフィールド)に使用し、Resultはエラー情報が必要な場合に使用します。
SwiftではResult { try throwingFunc() }を使用します — Resultコンストラクタはthrowsクロージャを受け取ります。KotlinではrunCatching { throwingFunc() }を使用し、Result<T>を返します。DartではResult<T>.tryCatch(() => throwingFunc())を使用します。これにより、例外ベースのコードをResultチェーンに簡単に統合できます。
成功値を変更せずにエラー型を変換するにはmapError(Swift)またはmapLeft(Dart/KotlinのEither)を使用します。両方のケースを処理して単一の値を返す必要がある場合はfoldを使用します。チェーンを中断せずにログを記録するには、onFailure(Kotlin)またはプリフィックス検査ポイントを使用します。
はい、ただし注意が必要です。KotlinのResult<T>は、K2コンパイラとリフレクションの特性により、suspend関数の戻り値の型として直接推奨されません。コルーチンで状態を表現するには、独自のsealed class NetworkResult<T>(Success、Error、Loading)を使用してください。ビジネスロジックの想定エラーには、ArrowのEitherがより強力な代替手段です。
foldは2つのコールバック(onSuccessとonFailure)を受け取り、任意の型の単一の値を返すメソッドです。switch式と同等ですが、高階関数として提供されます。foldはResultチェーンからの主要な終了点であり、Success/FailureをUiState、ユーザー向け文字列、または別のResultに変換します。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。