コンテンツへスキップ

Swiftのエラー処理を正しく行う:Objective-Cの負の遺産を乗り越える

「(YourError error 0)」のような意味不明なSwiftのエラーメッセージにうんざりしていませんか?明快でエレガントな方法で、この問題を根本から解決しましょう。

Swiftのエラー処理を正しく行う:Objective-Cの負の遺産を乗り越える

Swiftアプリで丁寧にエラーメッセージを作り込んだのに、実際には一度も表示されなかった――そんな経験はありませんか?代わりに、ユーザー(あるいはデバッグ中のあなた自身)が目にするのは、こんな意味不明なメッセージです:

“The operation couldn’t be completed. (YourApp.YourError error 0.)”

もし心当たりがあるなら、あなただけではありません。この混乱を招く挙動は、Swiftが登場して以来、初心者からエキスパートまで多くの開発者を悩ませてきました。今回は、なぜこのようなことが起きるのかを解説し、Swiftのエラー処理をより直感的にする解決策を紹介します。

Swift の Error プロトコルの意外な挙動

問題を示すシンプルな例を見てみましょう:

enum NetworkError: Error {
   case noConnectionToServer
   case parsingFailed

   var localizedDescription: String {
      switch self {
      case .noConnectionToServer:
         return "No connection to the server."
      case .parsingFailed:
         return "Data parsing failed."
      }
   }
}

// Using the error
do {
   throw NetworkError.noConnectionToServer
} catch {
   print("Error message: \(error.localizedDescription)")
   // Expected: "No connection to the server."
   // Actual: "The operation couldn't be completed. (AppName.NetworkError error 0.)"
}

何が起きたのでしょうか?明確なエラーメッセージを定義したにもかかわらず、Swiftはそれを完全に無視してしまいました!

なぜこうなるのか:NSError ブリッジ

この混乱の原因は、Swiftの Error プロトコルが内部的にObjective-Cの NSError クラスにブリッジされていることにあります。localizedDescription にアクセスすると、Swiftはあなたのプロパティを使わず、ドメイン(モジュール名)、コード(enumケースの整数値)、そしてデフォルトメッセージを持つ NSError を生成します。

Objective-Cとの相互運用性の観点では理にかなっているかもしれませんが、開発者体験としては最悪です。特にSwift初心者にとっては深刻な問題です。

「公式」の解決策:LocalizedError

Swiftには公式の解決策として LocalizedError プロトコルが用意されています。使い方はこうです:

enum NetworkError: LocalizedError {
   case noConnectionToServer
   case parsingFailed

   var errorDescription: String? { // Note: Optional String
      switch self {
      case .noConnectionToServer:
         return "No connection to the server."
      case .parsingFailed:
         return "Data parsing failed."
      }
   }

   // There are also these optional properties that are rarely used
   var failureReason: String? { return nil }
   var recoverySuggestion: String? { return nil }
   var helpAnchor: String? { return nil }
}

これで動作はしますが、いくつかの問題があります:

  • すべてのプロパティがオプショナルString?)なので、ケースの処理漏れがあってもコンパイラが教えてくれません

  • localizedDescription に影響するのは errorDescription だけで、他のプロパティは無視されがちです

  • どのプロパティが表示メッセージに影響するのか、名前から直感的にわかりません

  • Cocoaのエラー処理パターンに基づいたレガシーなアプローチのままです

より良い解決策:Throwable プロトコル

この問題に何度も悩まされた末、ErrorKit の一部としてシンプルな解決策を作りました。Throwable というプロトコルです:

public protocol Throwable: LocalizedError {
   var userFriendlyMessage: String { get }
}

このプロトコルにはいくつかの利点があります:

  • 単一の非オプショナルな要件のみ――ケースの漏れが起きません

  • userFriendlyMessage という名前が意図を明確に表しています

  • 互換性のために LocalizedError を継承しています(追加の作業は不要です!)

  • -able サフィックスでSwiftの命名規則に沿っています

使い方はこうです:

enum NetworkError: Throwable {
   case noConnectionToServer
   case parsingFailed

   var userFriendlyMessage: String {
      switch self {
      case .noConnectionToServer:
         return "Unable to connect to the server."
      case .parsingFailed:
         return "Data parsing failed."
      }
   }
}

// Using the error
do {
   throw NetworkError.noConnectionToServer
} catch {
   print("Error message: \(error.localizedDescription)")
   // Now correctly shows: "Unable to connect to the server." 🎉
}

Throwable を使えば、エラーメッセージが意図した通りに表示されます。サプライズはもうありません。

String Raw Value で素早く開発

ラピッドプロトタイピングでは、Throwable は文字列のraw valueとシームレスに連携します:

enum NetworkError: String, Throwable {
   case noConnectionToServer = "Unable to connect to the server."
   case parsingFailed = "Data parsing failed."
}

// That's it! No extra implementation needed

文字列のraw valueが自動的にエラーメッセージになり、初期開発時のボイラープレートを排除できます。後で本格的なローカライズの準備ができたら、userFriendlyMessage の完全な実装で String(localized:) に切り替えるだけです。

すぐに使えるエラー型

ボイラープレートをさらに削減するために、ErrorKitには一般的なシナリオ向けの組み込みエラー型が含まれています:

func fetchData() async throws {
    guard isNetworkAvailable else {
        throw NetworkError.noInternet
    }

    guard let url = URL(string: path) else {
        throw ValidationError.invalidInput(field: "URL path")
    }

    // More implementation...
}

組み込み型には以下のものがあります:

  • NetworkError:接続やAPIの問題に対応

  • FileError:ファイルシステム操作に対応

  • DatabaseError:データ永続化の問題に対応

  • ValidationError:入力バリデーションに対応

  • PermissionError:認可の問題に対応

  • その他多数…

各組み込み型はすでに Throwable に準拠しており、ローカライズされたユーザーフレンドリーなメッセージをすぐに利用できます。時間を節約しつつ、明確さも保てます。

GenericError で手軽なワンオフエラー

新しいエラー型を定義するまでもないけれど、カスタムメッセージが必要な場面では、ErrorKitの GenericError が便利です:

func quickOperation() throws {
    guard condition else {
        throw GenericError(userFriendlyMessage: "The operation couldn't be completed because a specific condition wasn't met.")
    }

    // More implementation...
}

初期開発や、専用のエラー型を作るほどでもないユニークなエラーケースに最適です。

より良いメッセージだけではない利点

Throwable を導入することで、エラーメッセージの改善だけでなく、さまざまな追加メリットが得られます:

  1. 新しい開発者にとっての明確さ: エラーメッセージの定義方法がプロトコルから明確にわかります

  2. コンパイル時の安全性: 非オプショナルの要件により、すべてのケースにメッセージがあることが保証されます

  3. ローカライズ対応String(localized:) と完璧に連携し、国際化をサポートします

  4. ボイラープレートの削減: 特に文字列raw valueと組み込み型との組み合わせで効果的です

  5. ユーザー体験の向上: 明確なエラーメッセージにより、ユーザーが何が起きたかを理解できます

  6. デバッグの効率化: 意味のあるエラーメッセージがデバッグを高速化します

移行方法

最も良い点は、ThrowableError のドロップイン置き換えであることです:

// Before
enum AppError: Error {
    case configurationFailed
}

// After
enum AppError: Throwable {
    case configurationFailed

    var userFriendlyMessage: String {
        switch self {
        case .configurationFailed:
           return "Failed to load configuration."
        }
    }
}

throwsdo/catch など、既存のエラー処理パターンはまったく同じように動作します。唯一の違いは、エラーメッセージが意図した通りに表示されるようになることです。

まとめ

Swiftのエラー処理は強力ですが、メッセージの扱いは長い間、混乱を招くペインポイントでした。Throwable プロトコルは、Swiftの設計原則に沿いながら、この長年の問題を解決するシンプルで直感的なソリューションを提供します。

エラー型に Throwable を採用することで、より明確なエラーメッセージ、ボイラープレートの削減、そしてより直感的な開発者体験が得られます。組み込みエラー型や GenericError フォールバックと組み合わせれば、期待通りに動作する包括的なエラー処理アプローチが完成します。

このアプローチをあなたのプロジェクトで試してみたい方は、Throwable プロトコル、組み込みエラー型、その他多くのSwiftエラー処理の改善を含むErrorKitをチェックしてください:

github.comFlineDev / ErrorKitSimplified error handling with built-in user-friendly messages for common errors. Fully localized. Community-driven

Swiftの開発でこのエラーメッセージの混乱に遭遇したことはありますか?どのように対処しましたか?ぜひソーシャルメディア(リンクは下記)で教えてください!

このシリーズの続きの記事:

  1. Swift 6のTyped Throwsの真の力をError Chainで引き出す

  2. Swiftアプリのエラーレポートを改善する:自動ログ+アナリティクス

  3. Swiftのエラーメッセージをみんなでユーザーフレンドリーにしよう

この記事が参考になりましたか?BlueskyMastodonでフォローして、Swiftのヒントやインディー開発の最新情報をチェックしてください。