Error Propagationとは、エラーが発生した場所からコールスタックを上ってハンドラまで伝播するメカニズムです。関数がエラーを自分で処理できない場合、例外(exception)、throws宣言、またはReturn Typeを通じて呼び出し元にエラーを渡します。モバイルアプリケーションの安定性には、propagationの適切な実装が不可欠です。未処理または誤って渡されたエラーはクラッシュの原因となります。Apple Swift Documentation (2026)によると、Swiftのthrowsによる自動伝播は、ボイラープレートコードなしで任意のレベルにエラーを渡すことを可能にします。
主要ポイント
Error Propagation(エラー伝播)とは、エラーオブジェクトが発生した関数からコールチェーンを上って、最寄りの適切なハンドラまで渡されるプロセスです。コールスタックを想像してください:ViewControllerがViewModelを呼び、ViewModelがRepositoryを呼び、RepositoryがAPIを呼びます。APIがネットワークエラーを返した場合、そのエラーはRepositoryとViewModelを経由してViewControllerに渡され、ユーザーにメッセージを表示します。各中間関数は、エラーを処理するか、さらに上に渡す(伝播する)かを決定します。
伝播には自動と手動の2つのアプローチがあります。自動アプローチ(Swift throws、Java checked exceptions)では、コンパイラが開発者にエラーの処理またはシグネチャでの伝播の宣言を強制します。手動アプローチ(Result Type、Kotlin Try)では、エラーは値として渡されます — 開発者は明示的にエラーを渡すか変換するコードを書きます。Kotlin Result Docs (2026)によると、KotlinのResult<T>は関数境界を直接越えた伝播を意図していません — 各レベルで変換または処理する必要があり、伝播はより意識的になりますが、より冗長にもなります。
アプローチの選択は、アプリケーションアーキテクチャと言語に依存します。Swiftではthrowsによる自動伝播が主流で、Kotlinでは例外(予期しないエラー用)とResultのようなコンテナ(予期されるエラー用)の混合が使用されます。理解すべき重要な点:伝播は目的ではなく、必要性です。理想的なアーキテクチャは伝播の深さを最小限に抑え、決定を下すのに十分なコンテキストがある可能な限り低いレベルでエラーを処理します。
Swiftでは、throwsによる伝播は自動です:throws付きの関数Aがthrows付きの関数Bを呼び出し、Aがdo-catchでBのエラーを処理しない場合、エラーは自動的にAの呼び出し元に渡されます。これにより、チェーンの各メソッドでthrowsを宣言する必要があるJava checked exceptionsに特有のボイラープレートコードが排除されます。Swiftは「チェーン内の1つのthrows関数 = チェーン全体がthrowsになる(中間レベルで処理されない場合)」という原則を使用します。
struct UserRepository {
func fetchUser(id: Int) throws -> User {
let data = try networkService.request(path: "/users/\(id)")
return try parseUser(from: data)
}
}
class UserViewModel {
let repo = UserRepository()
func loadUser(id: Int) throws -> User {
return try repo.fetchUser(id: id)
}
}
// ViewController — 最終ハンドラ
func onButtonTap() {
let vm = UserViewModel()
do {
let user = try vm.loadUser(id: 42)
updateUI(user)
} catch {
showError("ユーザーの読み込みに失敗しました")
}
}
伝播チェーン:networkService.request -> fetchUser -> loadUser -> onButtonTap。各中間関数はthrowsでマークされ、do-catchを含みません — エラーは自動的に上位に渡されます。ViewControllerのonButtonTapがdo-catchを持つ最終ハンドラです。ViewModelがエラーを変換する(別の型にラップする)場合、do-catchと新しいthrowを使用できます。自動伝播はコードを削減します:Repositoryはエラーの処理方法を知る必要はありません — それはユーザーにメッセージを表示するためのUIにアクセスできるViewControllerの責任です。
Kotlinでは、例外による伝播はシグネチャにthrows宣言を必要としません(すべての例外はuncheckedです)。例外はtry-catchに遭遇するまで自動的にスタックを上昇します。しかし、シグネチャにthrowsがないことで伝播が暗黙的になります:開発者は関数シグネチャから例外をスローする可能性があることを見えません。これは利点(ボイラープレートが少ない)であり欠点(処理を忘れやすい)でもあります。Kotlinはこの問題を、言語自体ではなく、規約とアーキテクチャパターンを通じて解決します。
class UserRepository(
private val api: ApiService,
private val db: Database
) {
suspend fun getUser(id: String): User {
return try {
api.fetchUser(id)
} catch (e: IOException) {
db.getCachedUser(id) ?: throw AppException("User unavailable")
}
}
}
class UserViewModel(private val repo: UserRepository) {
private val _state = MutableStateFlow<UiState<User>>(UiState.Loading)
val state: StateFlow<UiState<User>> = _state
fun loadUser(id: String) {
viewModelScope.launch {
try {
val user = repo.getUser(id)
_state.value = UiState.Success(user)
} catch (e: AppException) {
_state.value = UiState.Error(e.message ?: "Unknown")
}
}
}
}
Repositoryでの変換を伴う伝播:IOException(ネットワーク不可)が発生すると、関数はデータベースからキャッシュされたデータを取得しようとします。キャッシュが空の場合、AppExceptionをスローします — 伝播は新しいエラータイプで続行されます。ViewModelはAppExceptionをキャッチし、UiState.Errorに変換します — エラーはそれ以上進まず、UIレイヤーレベルで伝播が終了します。Kotlin Coroutinesは特性を追加します:launchの例外はCoroutineExceptionHandlerを通じて自動的に伝播し、asyncではawait()が呼び出された場合のみ伝播します。コルーチンでの伝播を設計する際にこれは重要です — SupervisorJobは子コルーチンのエラー時に親コルーチンのキャンセルを防ぎます。
例外に代わる方法は、成功またはエラーを値として渡すコンテナ型を通じた伝播です。このアプローチでは、関数は値ではなくラッパーを返します:SwiftのResult<T, E>、KotlinのResult<T>、DartのEither<L, R>(fpdartまたはdartzパッケージから)。エラーはスタックを巻き戻さず — 単にコンテナ内にあり、次のレベルがそれをどう処理するかを決定します。これにより伝播がより明示的で制御可能になります。
data class HttpResult<out T>(
val data: T?,
val error: AppError?
) {
val isSuccess: Boolean get() = data != null
val isError: Boolean get() = error != null
}
sealed class AppError {
data class Network(val message: String) : AppError()
data class Auth(val message: String) : AppError()
}
fun fetchUser(id: String): HttpResult<User> {
return try {
val response = api.get("/users/$id")
HttpResult(data = parseUser(response), error = null)
} catch (e: IOException) {
HttpResult(data = null, error = AppError.Network("No internet"))
}
}
HttpResult<T>はdataフィールドとerrorフィールドを持つシンプルなコンテナです。sealed class AppErrorはエラータイプ(Network、Auth)を定義します。fetchUser関数はHttpResultを返し、伝播にスタックの巻き戻しは必要ありません — 呼び出し元は単にisSuccess/isErrorをチェックします。このアプローチはClean Architectureで特に有用で、各レイヤー(data、domain、presentation)がエラーを変換できます:IOError -> DomainError -> UiError。コンテナを通じた伝播はこれらの変換を明示的でテスト可能にし、変換チェーンが関数シグネチャで見えない例外とは対照的です。
エラーハンドリング設計における重要な決定の1つは、伝播(上位に渡す)と処理(ここで処理する)の選択です。決定ルール:意味のあるアクションのための十分なコンテキストがあるレベルでエラーを処理する。UIにアクセスできる場合 — ユーザーにメッセージを表示します。キャッシュにアクセスできる場合 — 回復を試みます。どちらもない場合 — 伝播します。
| シナリオ | アクション | 根拠 |
|---|---|---|
| Repositoryでのネットワークエラー | 伝播 | Repositoryはユーザーがリクエストを再試行したいかどうかを知らない |
| Repositoryでの解析エラー | 処理(デフォルト値を返す) | Repositoryはフォーマットを知っており、フォールバック値を返せる |
| ViewModelでのタイムアウト | 処理(UiState.Error) | ViewModelはUiStateを管理し、エラーの変換方法を知っている |
| Interceptorでの認証エラー | 処理(トークン更新) | Interceptorはトークンにアクセスでき、セッションを復元できる |
| UseCaseでの不明なエラー | 伝播 | UseCaseにUIコンテキストはない — ビジネスロジックのみ |
黄金律:最小限の伝播、下位レベルでの最大限の処理。Repositoryがキャッシュから回復できる場合 — エラーを上位に渡さずにそれを行うべきです。ViewModelがSnackbarを表示できる場合 — ViewControllerからの追加コードを必要とせずに表示させるべきです。伝播の各レベルは結合を増やし、テストを複雑にします。Google Android Architecture Guide (2026)によると、ViewModelレベルでsealed class UiStateを使用してすべての可能な状態(Loading、Success、Error)を表現し、例外を直接UIレイヤーに渡さないことで、レイヤー境界を越えた伝播を最小限にすることを推奨しています。
誤った伝播は、モバイルアプリケーションで見つけにくいバグの原因です。開発者が直面する5つの主要な問題とその解決方法を見てみましょう。
最も一般的な問題:伝播中に例外がキャッチされ、ログに記録され、元の例外なしで新しい例外がスローされます。開発者はStackTraceを失い、エラーが正確にどこで発生したかを理解できません。Swiftではエラーチェイニングを使用します:throw MyError(context: originalError)。Kotlinでは:throw AppException(cause = originalException)。Dartでは:throw AppException(message, originalException)。cause/underlyingErrorを渡さずに新しい例外を作成しないでください。
catch (e: Exception) { /* 何もしない */ }は、アプリケーションが不正な状態で動作を続ける原因となるアンチパターンです。エラーを無視しても問題ないと確信している場合 — 正当化のコメントを追加してください。Swiftではオプショナルな無視にtry?(エラー -> nil)を使用します。Kotlinでは — Result<T>.onFailure { /* ログ */ }。ログなしで例外を飲み込まないでください。
エラーが5つ以上のレベルを処理されずに通過する場合、アーキテクチャの見直しが必要です。各伝播レベルは、下位関数のthrowsシグネチャへの依存です。解決策:レイヤー境界でFailureコンテナ(sealed class Result { Success, Error })を使用して、伝播を明示的かつ制限されたものにします。伝播チェーンが短いほど、コードのテストとデバッグが容易になります。
Kotlin Coroutinesでは、launchでの例外はデフォルトで親コルーチンとすべてのsibling(同じスコープの子)をキャンセルします。10の並列タスクの1つが失敗した場合、他の9つがキャンセルされ、これはしばしば望ましくありません。エラーを分離するにはSupervisorJobまたはsupervisorScopeを使用します:1つの子でのエラーはsiblingをキャンセルしません。ViewModelScopeはデフォルトでSupervisorJobを使用し、Androidでこの問題から保護します。
コールバックベースのAPIでは、エラーはしばしばコールバックのパラメータとして渡されます。コールバックがエラーを処理しない(または誤って処理する)場合、伝播は暗黙的になり、簡単に失われます。解決策:async/await(Swift)またはコルーチン(Kotlin)に移行します。そこでは伝播が標準のtry-catchメカニズムを通じて機能します。コールバックが避けられない場合 — 両方のケースを強制的に処理するためにEither<Error, T>またはResult<T>を使用します。
よくある質問
Throwは例外をスローする1回限りのアクションです。Error Propagationは、throwからcatchまで、複数のスタックレベルを通じてエラーを渡す全プロセスです。伝播には、throw、中間関数を通じた自動または手動の受け渡し、最終的な処理が含まれます。これはエラーのライフサイクルを説明するより広い概念です。
特定のシナリオで例外をスローするモックオブジェクトを使用します。関数がassertThrows(Kotlin/JUnit)またはXCTAssertThrowsError(Swift/XCTest)を通じてエラーを正しく伝播または処理するか確認します。Resultベースの伝播では、isSuccess/isErrorと両方のケースの値を確認します。
Result伝播は、1つのアーキテクチャ境界内の予期されるエラー(無効なデータ、ビジネスルール)に適しています。例外は、高レベルで処理されるべき予期しないエラー(ネットワーク損失、I/Oエラー)に適しています。エラーのある結果は実行フローを中断しませんが、例外は中断します。
Kotlin Coroutinesでは、launchの例外はsiblingのキャンセルを伴って自動的にCoroutineScopeを通じて伝播します。分離にはsupervisorScopeまたはSupervisorJobを使用します:1つのコルーチンのエラーは他をキャンセルしません。asyncの場合、await()呼び出し時にtry-catchを通じて明示的にエラーを処理する必要があります。そうしないと飲み込まれます。
理想的な最終ハンドラはUIレイヤー(ViewController、Fragment/Composable)です。それだけがユーザーインターフェースにアクセスでき、メッセージ、Snackbar、またはダイアログを表示できます。中間レイヤー(Repository、UseCase、ViewModel)はエラーを伝播し、必要に応じてより抽象的なドメインタイプに変換します。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。