🍁
Swift: Errorを構造化して使う
アプリ独自のエラーのハンドリングをしていて、エラーのコンテキストは複数あって分類/整理しながら使いたいけれど、エラーの型そのものはまとめて扱いたいと思いました。
例えば、
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