🍁

Swift: Errorを構造化して使う

2022/12/01に公開

アプリ独自のエラーのハンドリングをしていて、エラーのコンテキストは複数あって分類/整理しながら使いたいけれど、エラーの型そのものはまとめて扱いたいと思いました。

例えば、

enum MyAppError: Error {
    // アカウント系エラー
    case noAccountWithSpecifiedId
    case accountAlreadyRegistered(_ username: String)
    case failedToGetAccountInfo
    // 認証系エラー
    case authInvalidCallbackURL
    case authFailedToStartSession
    case authFailedToGetParameters
    // 添付ファイル系エラー
    case attachedFileBroken(_ filename: String)
    case attachedFileNotSupported(_ fileExtension: String)
    case attachedFileExceeded
}

のように複数系統のエラーをひとまとめにしていると、エラーの数が増えてきた時に管理がしづらいです。ですが、これらを別々のErrorにしてしまうと、APIとして一括してエラーをハンドリングしづらくなるため、うまいこと構造化したいです。

構造化

そこで、Errorの中でErrorを持つようにしてみました。

enum MyAppError: Error {
    enum Account: Error {
        case noAccountWithSpecifiedId
        case alreadyRegistered(_ username: String)
        case failedToGetInfo
    }

    enum Auth: Error {
        case invalidCallbackURL
        case failedToStartSession
        case failedToGetParameters
    }

    enum AttachedFile: Error {
        case broken(_ filename: String)
        case notSupported(_ fileExtension: String)
        case exceeded
    }

    case account(_ detail: Account)
    case auth(_ detail: Auth)
    case attachedFile(_ detail: Auth)
}

こうすると、throw MyAppError.account(.alreadyRegistered)のようにエラーを投げられるようになります。

さらに、ErrorではなくLocalizedErrorに準拠させることでMyAppErrorから直接 .errorDescriptionにアクセスできるようになります。

enum MyAppError: LocalizedError {
    enum Account: LocalizedError {
        case noAccountWithSpecifiedId
        case alreadyRegistered(_ username: String)
        case failedToGetInfo

        var errorDescription: String? {
            switch self {
            case .noAccountWithSpecifiedId:
                return "No account with the specified ID was found."
            case .alreadyRegistered(let username):
                return "Account (@\(username)) is already registered."
            case .failedToGetInfo:
                return "Failed to get account info."
            }
        }
    }

    enum Auth: LocalizedError {
        case invalidCallbackURL
        case failedToStartSession
        case failedToGetParameters

        var errorDescription: String? {
            switch self {
            case .invalidCallbackURL:
                return "Could not get the callbackURL."
            case .failedToStartSession:
                return "Failed to start session."
            case .failedToGetParameters:
                return "Failed to get parameters."
            }
        }
    }

    enum AttachedFile: LocalizedError {
        case broken(_ filename: String)
        case notSupported(_ fileExtension: String)
        case exceeded

        var errorDescription: String? {
            switch self {
            case .broken(let fileName):
                return "Attached file: \(fileName) may be broken."
            case .notSupported(let fileExtension):
                return "MyApp does not support \(fileExtension) format."
            case .exceeded:
                return "The number of files you can attach is exceeded."
            }
        }
    }

    case account(_ detail: Account)
    case auth(_ detail: Auth)
    case attachedFile(_ detail: Auth)

    var errorDescription: String? {
        switch self {
        case .account(let detail):
            return detail.errorDescription
        case .auth(let detail):
            return detail.errorDescription
        case .attachedFile(let detail):
            return detail.errorDescription
        }
    }
}
使用例
func hoge() throws {
    // 何か処理
    throw MyAppError.account(.noAccountWithSpecifiedId)
}

do {
    try hoge()
} catch {
    // ここで、localizedDescriptionがちゃんと機能する
    Swift.print(error.localizedDescription)
}

Discussion