FriendlyErrorType

新しいエラーハンドリング

Swift 2でthrowを使ったエラーハンドリングが新たに導入された。従来のNSErrorを使ったエラーハンドリングの問題点は、メソッドNSErrorポインタの代わりにnilを渡すことで無視できてしまうことだった。新たに導入されたエラーハンドリングでは、throwsキーワードが宣言されたメソッドを呼び出す際にdo-catch文で囲うことを強制される。throwで投げられるエラーはNSErrorではなくErrorTypeというprotocolを実装した値だ。Cocoaフレームワーク内のNSErrorを使っていたメソッドthrowsを使うように置き換えられており、今後は独自のエラーを定義する場合はNSErrorではなくErrorTypeを使うのが望ましいと考えられる。しかし、ErrorTypeにも問題点はあり現実的な設計方針を検討する必要がある。

アプリ独自エラーの実装

NSErrorの代わりにErrorTypeを使っていく流れがあるものの、ErrorTypeにはNSErrorが持っていたlocalizedDescriptionuserInfoといったエラー情報がないという問題点がある。そこで、ErrorTypeを継承した新たなprotocolを定義するという方針を考えてみた。

protocol FriendlyErrorType: ErrorType {
    var summary: String { get }
    var reason: String? { get }
    var suggestion: String? { get }
}

このFriendlyErrorTypeを使って以下のように独自エラーを定義できる。

enum ApplicationError: FriendlyErrorType {
    case SomethingWrong
    case DecodeFailed([String])

    var summary: String {
        switch self {
        case .SomethingWrong:
            return "Something wrong"
        case .DecodeFailed(_):
            return "Decode failed"
        }
    }

    var reason: String? {
        switch self {
        case .SomethingWrong:
            return .None
        case .DecodeFailed(let fields):
            let failedFields = fields.joinWithSeparator(", ")
            return "Failed to decode following fields: \(failedFields)"
        }
    }
    
    var suggestion: String? {
        switch self {
        case .SomethingWrong:
            return .None
        case .DecodeFailed:
            return .None
        }
    }
}

また、CocoaフレームワークメソッドErrorTypeを投げるようになったものの、Alamofire等のライブラリを使う際にはNSErrorを使うことになるため、FriendlyErrorTypeを実装するようにNSErrorを拡張する。

extension NSError: FriendlyErrorType {
    var summary: String {
        return localizedDescription
    }
    
    var reason: String? {
        return userInfo[NSLocalizedFailureReasonErrorKey] as? String
    }
    
    var suggestion: String? {
        return userInfo[NSLocalizedRecoverySuggestionErrorKey] as? String
    }
}

なぜprotocol extensionではなく継承なのか

protocol extensionだとErrorTypeにデフォルトの実装を与えることになる。その場合、ErrorTypeとして渡されたエラーに対してメソッドを呼ぶと、すべてそのデフォルトの実装の結果が返るようになる。一方、FriendlyErrorTypeはただのprotocolなので、メソッドの結果はメソッドを実装する各クラスの結果を反映する。

extension ErrorType {
    var summary: String {
        return ""
    }
}

extension NSError {
    var summary: String {
        return localizedDescription
    }
}

let error: ErrorType = NSError(domain: "com.github.naoty.playground", code: 1000, userInfo: [NSLocalizedDescriptionKey: "Something wrong"])
print(error.summary) //=> "\n"
protocol FriendlyErrorType: ErrorType {
    var summary: String { get }
}

extension NSError: FriendlyErrorType {
    var summary: String {
        return localizedDescription
    }
}

let error: FriendlyErrorType = NSError(domain: "com.github.naoty.playground", code: 1000, userInfo: [NSLocalizedDescriptionKey: "Something wrong"])
print(error.summary) //=> "Something wrong\n"

エラーの利用例

FriendlyErrorTypeを実装したエラー型を実際に利用してみる。Alamofire、SwiftTask、Himotokiを使ってQiita APIを呼び出している。

return Task<Void, [Item], FriendlyErrorType> { progress, fulfill, reject, configure in
    Alamofire.request(.GET, "https://qiita.com/api/v2/items").responseJSON { response in
        switch response.status {
        case .Success(let value):
            if let objects = value as? [AnyObject] {
                var items: [Item] = []
                for object in objects {
                    do {
                        let item = try decode(object) as Item
                        items.append(item)
                    } catch DecodeError.MissingKeyPath(let keyPath) {
                        reject(ApplicationError.DecodeFailed(keyPath.components))
                    } catch {
                        reject(ApplicationError.SomethingWrong)
                    }
                }
                fulfill(items)
            } else {
                reject(ApplicationError.DecodeFailed(["root"]))
            }
        case .Failure(let error):
            reject(error)
        }
    }
}

NSErrorを拡張しているため、ApplicationErrorNSErrorFriendlyErrorTypeとして並べて扱うことができている。

FriendlyErrorTypeを使ってアラートを表示する実装は以下のようなイメージだ。

let title = error.summary

var message = ""
if let reason = error.reason {
    message += reason
    message += "\n"
}
if let suggestion = error.suggestion {
    message += suggestion
}

let alertController = UIAlertController(title: title, message: message, preferredStyle: .Alert)
presentViewController(alertController, animated: true, completion: nil)

以上のような方針に基づいたサンプルアプリケーションを用意した。

関連記事