Swift & Objective-C

[Swift基礎 #5] throws・try・Resultの選び方

Swiftのエラー処理ツールは、失敗をいつ処理するか、どの情報を保持するかによって役割が異なります。throws・do-catch・try?・try!・Resultの選び方を、実行結果とともに整理します。

読了 8 分
[Swift基礎 #5] throws・try・Resultの選び方のカバー画像

Swiftのエラー処理を学んでいると、ツールが多すぎると感じることがあります。throwsとdo-catch、tryに付く疑問符と感嘆符、さらにResult型まであります。エラーをthrowするAPIもあれば、Resultを返すAPIもあり、try?でそのまま握りつぶすコードもあります。標準はどれなのでしょうか。

実際には、これらは競合するものではなく役割分担の関係です。基本はthrows、tryのバリエーションは「エラーにどれだけ関心があるか」のスペクトラム、Resultはエラーを値として持ち運ぶ必要があるときの補完手段です。Swift基礎シリーズ第5回では、この地図を描きます。

入力検証で失敗経路を先に閉じる構造は、前回のSwiftのguardと早期脱出の選び方につながります。

基本 — エラーも型、throwも契約

Swiftのエラー処理は、2つの宣言から始まります。エラーはErrorプロトコルに準拠する型として定義し、エラーを発生させる可能性のある関数はシグネチャにthrowsを付けます。

enum PaymentError: Error {
    case insufficientBalance(needed: Int)
    case cardExpired
    case network(underlying: Error)
}

func pay(amount: Int) throws -> Receipt {
    guard balance >= amount else {
        throw PaymentError.insufficientBalance(needed: amount - balance)
    }
    // ...
}

エラー型にenumがよく使われるのは偶然ではありません。Optionalの記事で見たように、enumは「場合分け」を表す道具であり、失敗理由こそ場合分けだからです。関連値を使えば、不足額のようなコンテキスト情報も持たせられます。

さらに重要なのは、シグネチャにthrowsがあることです。この関数が失敗する可能性が型システムに登録されます。そのため呼び出し側は必ずtryを使う必要があり、tryを省くとコンパイルエラーになります。例外がどこで発生するか分からない言語とは異なり、Swiftコードでは失敗する可能性のある箇所がすべてtryで示されます。Optionalが「値がない」ことを型に昇格させたように、throwsは「失敗する可能性」をシグネチャに昇格させるのです。第1回の安全優先の原則がここでも繰り返されます。

受け側の基本形はdo-catchです。

do {
    let receipt = try pay(amount: 50_000)
    show(receipt)
} catch PaymentError.insufficientBalance(let needed) {
    showTopUp(needed: needed)
} catch {
    showError(error)  // 残りすべて, error 変数を自動提供
}

catchはパターンマッチングです。特定のケースだけを捕捉し、関連値を取り出し、残りは最後のcatchで受ける構造はswitchに似ています。エラーがenumであることと結び付いた設計です。

tryの3つの顔 — エラーへの関心のスペクトラム

tryには3つの表記があり、それぞれ「エラーをどう扱うか」を宣言します。

try — 処理するか、伝播させる。 これが基本形です。do-catchで捕捉するか、自分の関数もthrowsとして宣言し、エラーを上位へ流します。エラー伝播は明示的なコードなしに自動で行われ、これがSwiftのエラー処理の隠れた強みです。中間層の関数はthrowsを付けるだけで無料で配管の役割を果たし、処理はUIに近い外側の層で一度行えば済みます。

try? — 失敗しても構わない。値がなければよい。 エラーをOptionalに変換します。成功すれば値、失敗すればnilになり、エラー情報は破棄されます。キャッシュの読み込みのような「失敗したらそれまで」というシナリオに適しています。危険なのは習慣的なtry?です。失敗理由が必要な場所でtry?を使うと、デバッグの手掛かりが静かに消えます。「この失敗の理由を誰も気にしないか」に「はい」と答えられるときだけ使うのが基準です。

try! — 失敗はプログラマーのバグである。 失敗すると即座にクラッシュします。強制アンラップ!とまったく同じ考え方で、アプリバンドル内のリソース読み込みのように「失敗するならコードが間違っている」場所でのみ許可されます。Optionalの記事で立てた基準がそのまま適用されます。nil(ここではエラー)が通常のシナリオなら、絶対に禁止です。

tryの3つのバリエーションは、エラーへの向き合い方を宣言します
tryの3つのバリエーションは、エラーへの向き合い方を宣言します

Result — エラーを値として持ち運ぶ

throwsが基本なら、Resultはいつ使うのでしょうか。Resultは成功または失敗を格納するenumです。

enum Result<Success, Failure: Error> {
    case success(Success)
    case failure(Failure)
}

throwsとの決定的な違いは、時間と場所です。throwsは呼び出し直後の処理(または伝播)を強制する制御フローですが、Resultは通常の値なので、保存したり配列に入れたり、後で処理したりできます。そのためResultが適する場面は、おおむね3つあります。

1つ目は、completion handlerベースの非同期APIです。クロージャの記事で見たように、completion handlerは関数が返った後に実行されるため、throwsでエラーを渡せません。completion: (Result<Data, NetworkError>) -> Voidがその場面の標準でした。2つ目は、結果を集計するときです。10個のタスクを実行して成功7個、失敗3個を集計するには、エラーが値である必要があります。3つ目は、失敗型を明示したいときです。ResultのFailureは具体型なので、どのようなエラーが来るかがシグネチャから分かります。

ただし、方向性は押さえておく必要があります。async/awaitが標準になってから、非同期エラーの伝達はasync throwsが担うようになり、Resultの1つ目の用途は新しいコードで減少しています。3つ目の用途もSwift 6のtyped throws(throws(PaymentError)、SE-0413)が取り込もうとしています。現在の実務上の基準はこうです。基本はthrows、Resultは「エラーを値として保存・集計・伝達する必要がある」特殊な状況のためのツールです。

ちなみに、両者の変換は1行でできます。Result { try pay(amount: 100) }で包み、try result.get()で取り出します。境界で自由に行き来できるので、どちらか一方に固執する理由はありません。

設計感覚 — 良いエラーは受け手を考える

ここからは文法の外側の話を少しします。エラー処理コードの品質は、投げる側の設計で大きく決まります。

エラーは、呼び出し側が異なる対応を取れる単位に分けます。 ケースを細分化する基準は、「受け側がこの2つを異なるように処理するか」です。残高不足とカード期限切れはユーザーへの案内が異なるため別ケースが適切です。一方、TCPタイムアウトとDNS失敗をアプリが同じように再試行するなら、networkにまとめるべきです。処理方法が同じエラーを10種類に分けても、catchが増えるだけです。

throwするか、Optionalを返すかの基準も同じです。 「ない」ことが辞書検索のように予想可能な日常の結果ならOptional、何かが間違っていて理由が必要ならthrowsです。理由が1つだけで明白なら、Optionalで十分な場合が多くあります。

ユーザーに表示するメッセージは、エラー型と一緒に設計します。 LocalizedErrorに準拠すれば、エラー自身に表示用メッセージを持たせられます。この点は別の記事で詳しく扱っています。

throwsはすぐ受け取るべきボール、Resultは箱に入った値
throwsはすぐ受け取るべきボール、Resultは箱に入った値

実際に実行して確認した結果

Apple Swift 6.3.3で、文字列を整数に変換するthrowing関数を実行しました。失敗をtry?で受けるとnilだけが残り、成功をResultでキャプチャすると、後でget()によって再びthrowingフローに接続できました。

error=try?-nil:true,result:42

この違いがあるため、失敗理由がログ、再試行、ユーザーへの案内のいずれかを変え得る場合、私はtry?を使いません。反対に、キャッシュ検索のように失敗と不在を同じものとして扱う境界では、nilへの変換のほうが意図を明確に示します。Resultは、すぐに処理する必要のない結果を保存するときや、複数のタスクの結果を集めるときだけ選びます。

まとめ

  • Swiftのエラー処理の骨格は、Errorプロトコル(主にenum)+throwsシグネチャ+do-catchによるパターンマッチングです。失敗の可能性が型システムに登録され、失敗箇所はすべてtryで示されます。
  • tryの3つのバリエーションはエラーへの態度の宣言です。処理・伝播はtry、理由が気にならないならtry?、失敗がそのままバグならtry!です。
  • エラー伝播は自動です。中間層はthrowsだけを付け、処理は外側で一度行います。
  • Resultはエラーを値として保存・集計するときのツールです。非同期の伝達用途はasync throwsへ、型の明示用途はtyped throwsへと世代交代しつつあります。
  • エラーケースを「呼び出し側が異なる対応を取る単位」に分けることが、設計の核心です。

これでSwift基礎シリーズの制御フロー三部作(Optional、guard、エラー処理)が完成しました。次回は少し趣の異なる基礎、SwiftのStringが他の言語より難しい理由です。「韓国語は何文字か」という問いがなぜ簡単ではないのか、というところから始めます。

あわせて読みたい

出典と確認基準

  • The Swift Programming Language: Error HandlingSwift.org · 公式ドキュメント · 確認日 2026年8月26日根拠: Error、throw、throws、do-catch、try・try?・try!の処理方法
  • Swift ResultApple Developer Documentation · 公式ドキュメント · 確認日 2026年8月26日根拠: Resultのsuccess・failure表現とthrowing式の変換API