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 を導入することで、エラーメッセージの改善だけでなく、さまざまな追加メリットが得られます:
新しい開発者にとっての明確さ: エラーメッセージの定義方法がプロトコルから明確にわかります
コンパイル時の安全性: 非オプショナルの要件により、すべてのケースにメッセージがあることが保証されます
ローカライズ対応:
String(localized:)と完璧に連携し、国際化をサポートしますボイラープレートの削減: 特に文字列raw valueと組み込み型との組み合わせで効果的です
ユーザー体験の向上: 明確なエラーメッセージにより、ユーザーが何が起きたかを理解できます
デバッグの効率化: 意味のあるエラーメッセージがデバッグを高速化します
移行方法
最も良い点は、Throwable が Error のドロップイン置き換えであることです:
// Before
enum AppError: Error {
case configurationFailed
}
// After
enum AppError: Throwable {
case configurationFailed
var userFriendlyMessage: String {
switch self {
case .configurationFailed:
return "Failed to load configuration."
}
}
}throws、do/catch など、既存のエラー処理パターンはまったく同じように動作します。唯一の違いは、エラーメッセージが意図した通りに表示されるようになることです。
まとめ
Swiftのエラー処理は強力ですが、メッセージの扱いは長い間、混乱を招くペインポイントでした。Throwable プロトコルは、Swiftの設計原則に沿いながら、この長年の問題を解決するシンプルで直感的なソリューションを提供します。
エラー型に Throwable を採用することで、より明確なエラーメッセージ、ボイラープレートの削減、そしてより直感的な開発者体験が得られます。組み込みエラー型や GenericError フォールバックと組み合わせれば、期待通りに動作する包括的なエラー処理アプローチが完成します。
このアプローチをあなたのプロジェクトで試してみたい方は、Throwable プロトコル、組み込みエラー型、その他多くのSwiftエラー処理の改善を含むErrorKitをチェックしてください:
Swiftの開発でこのエラーメッセージの混乱に遭遇したことはありますか?どのように対処しましたか?ぜひソーシャルメディア(リンクは下記)で教えてください!

