Log

【Swift】エラーハンドリングと文字列

iOS開発でエラーハンドリング時のエラーメッセージの実装が間違えているような気がしたのでその調査メモです.
恥ずかしながら今まではCustomStringConvertibleやコンバータークラスを作成していました.

間違い等ありましたら指摘してくださると嬉しいです.

ErrorとNSError

Foundation/UIKit等を利用していると稀にNSErrorというものを見かけます.
SwiftのErrorとの違いは次の点が違います.

Error

Errorはdo-catch構文やResult型で補足することができる抽象型のプロトコルです.
Swiftで定義されているErrorプロトコルに準拠したオブジェクトを例外として投げることが可能で,クラスや共用型含む列挙型等に準拠させることができます.

NSError

Foundationで定義されたクラスで,エラーの発生とそのエラー情報を伝える使い方をします.
クラスはNSObjectを継承したクラスですが,SwiftのErrorプロトコルにも準拠しているためErrorと同様の使い方ができます.

以上がErrorとNSErrorの違いですが,一言で表すと
Errorはエラーの発生を知らせる事が可能.NSErrorはエラーの発生とその情報もセットで知らせることが可能.
という認識をしておけば大丈夫です.

NSError エラー情報

NSErrorはエラーの情報も格納できると上述しました.
エラーの情報は次のプロパティで取得することが可能です.

class NSError: NSObject {
    /// ローカライズされたエラーを説明する文字列
    var localizedDescription: String { get }

    /// ローカライズされたアラートの選択肢として表示する文字列
    var localizedRecoveryOptions: [String]? { get }

    /// ローカライズされたエラーへの対処方法を表す文字列
    var localizedRecoverySuggestion: String? { get }

    /// ローカライズされたエラーの発生原因を表す文字列
    var localizedFailureReason: String? { get }
}

ローカライズされているということは,UIへの表示を前提にされたものだということが推測できます.
API呼び出しなどではなくNSErrorのインスタンスを生成したい場合はuserInfoにローカライズされた文字列を格納します.

let nsError = NSError(domain: "nserror", code: 1, userInfo: [
    NSLocalizedDescriptionKey: "localized description",
    NSLocalizedRecoveryOptionsErrorKey: ["localized recovery options"],
    NSLocalizedRecoverySuggestionErrorKey: "localized recovery suggestion",
    NSLocalizedFailureReasonErrorKey: "localized failure reason"
])

print(nsError.localizedDescription)
print(nsError.localizedRecoveryOptions)
print(nsError.localizedRecoverySuggestion)
print(nsError.localizedFailureReason)

macOS向けネイティブアプリ開発に用いられるCocoaでは アラートを表示するAPIの引数にNSErrorを渡すことでラベル等に適切に表示してくれるものもあります.

let nsError = ...
NSAlert(error: nsError).runModal()

NSAlertでエラーを表示

LocalizedError

FoundationのNSErrorのようにエラー情報を保持可能で,SwiftのプロトコルとしてErrorを利用したい場合はLocalizedErrorというFoundationで定義されたプロトコルが利用可能です.

LocalizedErrorは次のプロパティが定義された,Errorを継承したプロトコルです.

public protocol LocalizedError : Error {
    var errorDescription: String? { get }

    var failureReason: String? { get }

    var recoverySuggestion: String? { get }

    var helpAnchor: String? { get }
}

先程の例として載せたNSErrorのプロパティと雰囲気が似ていると思います.
実際にプロトコルに準拠させた列挙型を定義して実際に利用してみると次のようになります.

enum MyError: LocalizedError {
    case a
    case b
    
    var errorDescription: String? { "error description" }
    var failureReason: String? { "failure reason" }
    var recoverySuggestion: String? { "recovery suggestion" }
    var helpAnchor: String? { nil }
}

do {
    throw MyError.b
}
catch {
    // MyError.errorDescription
    let myError = error as! MyError
    print(myError.errorDescription)

    // Error.localizedDescription
    print(error.localizedDescription)
}

// 両方とも"error description"が出力

定義したMyErrorのlocalizedDescriptionはどのケースでも"error description"としています.
例を見ると引数の異なるprint()が2回呼び出されていますが,その出力は両方とも同じです.
FoundationのextensionでErrorプロトコルlocalizedDescriptionというプロパティが追加されており,エラーの実装がNSErrorでもErrorでも意図した文字列(ローカライズされたエラーの説明)を返すという動作をします.

localizedDescriptionがNSErrorとErrorの違いを吸収してくれるので,利用者はキャスト等の処理を挟まなくて済みます.

NSErrorと同じくCocoaAPIのAlertにMyError.bを渡してあげると,意図した形でアラートが表示されたことが確認できました.

NSAlert(error: MyError.a).runModal()

NSAlertでカスタムエラーを表示

まとめ

Swiftのエラーハンドリングで型を定義する際にはLocalizedErrorを使いましょう.
エラーメッセージはError.localizedDescriptionで定義しておくと,NSErrorとの違いを吸収してくれるので積極的に利用したいです.

参照

Errors and Exceptions | Apple Developer Documentation