# Swiftのエラー処理
プログラミングにおけるエラー処理とは、プログラム内で発生したエラーをキャッチし、そこから回復、または終了するプロセスのことをいいます。システムを構築する際は、この一連の作業が必要不可欠になります。なぜならプログラムは常に想定どおりに動くとは限らないからです。
例えば、ファイルからデータを読み込んで処理するプログラムを考えてみましょう。このプログラムは、様々な要因で失敗する可能性があります。ファイルが存在しない、読み取り権限がない、互換性のある形式でエンコードされていない、などの理由で実行が止まってしまうことがあります。そして、プログラムが止まってしまったら適切にエラー処理をして、ユーザーに応答する必要があります。
この適切にエラー処理をするためにSwiftは、いくつかの機能を提供しています。エラーは様々な状況で発生し得ます。多様な状況に適切な手段を使って、処理をすることが重要になります。
# エラー処理の種類
Swiftで、エラーを処理するために次のような機能が提供されています。これらを状況別に適切に使えるように見ていきましょう。
# do-catch文
do-catch文
はSwift標準のエラー処理機能です。do-catch文を使用すると、コードのブロック内で発生したエラーを処理することができます。do
節のコードでエラーが発生した場合、それを catch
節と照合して、パターンがマッチしたら、エラーを処理します。
# 定義方法
do-catch文は、次のように do
を宣言し、{}
にコードを記述します。catch
には、パターンを記述してマッチした場合にエラーを処理します。do節の実行中にエラーが発生すると、catch節に実行が移ります。
do {
処理の実行
} catch パターン1 {
} catch パターン2 where 条件 {
} catch {
}
次の例では、throw
文でエラーを発生させています。catch節の中では、定数 error
を使って発生したエラーの詳細情報を参照することができます。
struct APIError: Error {}
do {
print("処理を実行しました1")
// throwでエラーを投げる
throw APIError()
// throw するのでここは呼ばれない
print("処理を実行しました2")
} catch {
// エラーをキャッチ
print(error)
}
/* 実行結果 */
// 処理を実行しました1
// APIError()
# パターンマッチ
パターンマッチを使うとさらに条件を絞って、エラーを処理することができます。
次の例では、APIErrorにエラー処理のケースを定義しています。パターンマッチを使ったときのcatch節では、定数 error
を使うことはできません。明示的に定義した定数を参照して発生したエラーの詳細情報を取得することができます。
enum APIError: Error {
case network
case unexpected(String)
}
do {
throw APIError.unexpected("予期せぬエラーです")
// パターンマッチでエラーをキャッチする
} catch APIError.unexpected(let unexpectedError) {
// 定数 error は使えない
// 明示的に定数 unexpectedError を使う
print(unexpectedError)
}
/* 実行結果 */
// 予期せぬエラーです
# where
次の例では、where
キーワードを使って条件を追加しています。データベースエラーが発生し、その引数の値を見てエラー処理をしています。
enum DBError: Error {
case db(Int)
}
do {
throw DBError.db(404)
} catch DBError.db(let status) where status == 404 {
print("NOT FOUND ERROR")
}
/* 実行結果 */
// NOT FOUND ERROR
# throw
throw
文は、任意のタイミングでエラーを発生させるときに使います。throw文でエラーを投げるためには、Errorプロトコルに準拠させる必要があります。
// Errorプロトコルに準拠させる
struct MyError: Error {}
do {
// throw キーワードでエラーを投げる
throw MyError()
} catch {
print(error)
}
# Error プロトコル
Swiftでエラーを投げるためには、Errorプロトコルに準拠させてエラーを定義する必要があります。Errorプロトコルはインターフェースを満たすような実装は必要ありません。
準拠させるのは主に列挙型が一般的になっており、一つの列挙型に全てのエラーを定義するのではなく、エラーの種類に合わせてそれぞれ定義するのが推奨されています。
// 推奨
enum DBError: Error {
case notFound
case timeout
}
enum APIError: Error {
case invalid
case unexpected
}
// 非推奨
enum Errors: Error {
case networkTimedOut
case dbNotFound
case server
case unexpected
}
# throws
do-catch文の中で、throwキーワードを使ってエラーを発生させることができました。 throwは、do-catch文以外でも関数やメソッド内で使うことができます。
関数内でthrowを使うには、throws
キーワードを宣言する必要があります。
enum SomeError: Error {
case unexpected(String)
}
// throws キーワードを宣言する
func method() throws -> Void {
print("関数が呼びされました")
throw SomeError.unexpected("予期せぬエラーです")
}
そして、呼び出す側は try
キーワードを使って関数を呼び出し、エラーを投げることができます。
do {
try method()
} catch SomeError.unexpected(let error) {
print(error)
}
/* 実行結果 */
// 予期せぬエラーです
# ネストされている場合
例えば、呼び出した関数のさらに先で別の関数がエラーを投げている場合、次のように続けてdo-catch文の中でtryをする必要があります。
enum SomeError: Error {
case unexpected(String)
}
do {
try method1()
} catch SomeError.unexpected(let error) {
print(error)
}
func method1() throws -> Void {
do {
try method2()
}
}
func method2() throws -> Void {
do {
try method3()
}
}
func method3() throws -> Void {
print("関数が呼びされました")
throw SomeError.unexpected("予期せぬエラーです")
}
catch節を書かずに関数を実行すると、エラーの処理をせずに、呼び出し元を遡ってcatch節があるところに移ります。上記の例だと、method3
が投げたエラーが 一番上の呼び出し元の catch SomeError.unexpected(let error)
で 処理されることになります。
もし、method1
でエラーをキャッチしたいなら以下のようにcatch節を書きます。
enum SomeError: Error {
case unexpected(String)
}
do {
try method1()
}
func method1() throws -> Void {
do {
try method2()
// ここでエラーをキャッチする
} catch SomeError.unexpected(let error) {
print(error)
}
}
func method2() throws -> Void {
do {
try method3()
}
}
func method3() throws -> Void {
print("関数が呼びされました")
throw SomeError.unexpected("予期せぬエラーです")
}
# イニシャライザ
イニシャライザを定義するときも throws
キーワードを使って、エラーを投げることができます。
次の例では、構造体のイニシャライザで引数を見てエラーを発生させています。
enum ItemError: Error {
case invalid(String)
}
struct Item {
let quantity: Int
// throws キーワードを宣言
init(quantity: Int) throws {
if quantity < 5 {
throw ItemError.invalid("エラー")
}
self.quantity = quantity
}
}
do {
let item1 = try Item(quantity: 1)
} catch ItemError.invalid(let error) {
print(error)
}
/* 実行結果 */
// エラー
# rethrows
rethrows
キーワードは、引数として受け取ったクロージャがエラーをもっていて、その呼び出し元にエラーを返したいときに使います。
次の例では、関数の中でエラーをもつクロージャを実行しています。関数の中ではなく、呼び出し元でエラーをキャッチしたいので rethrows
キーワードを宣言しています。
enum SomeError: Error {
case unexpected(String)
}
// rethrows を宣言してエラーを呼び出し元へ返す
func method(closure: () throws -> Void) rethrows {
try closure()
}
do {
// エラーを発生させるクロージャを渡す
try method() {
throw SomeError.unexpected("エラー")
}
} catch SomeError.unexpected(let error) {
print(error)
}
/* 実行結果 */
// エラー
rethrows
キーワードが指定されている関数の中で、クロージャとは関係のないエラーを発生させることはできません。関係のないエラーを投げると、コンパイラはエラーを出力します。
enum SomeError: Error {
case unexpected(String)
}
func method(closure: () throws -> Void) rethrows {
// クロージャとは関係のないエラーを発生させる
method2()
}
func method2 throws {
throw SomeError.unexpected("エラー")
}
do {
try method() {
throw SomeError.unexpected("エラー")
}
} catch SomeError.unexpected(let error) {
print(error)
}
/* 実行結果 */
// error: call can throw, but the error is not handled; a function declared 'rethrows' may only throw if its parameter does
// try method2()
# try
エラーをもつ関数を実行するには、try
キーワードを使います。
enum SomeError: Error {
case unexpected(String)
}
func method() throws -> Void {
throw SomeError.unexpected("予期せぬエラーです")
}
do {
// try キーワードで関数を実行する
try method()
} catch SomeError.unexpected(let error) {
print(error)
}
try
キーワードがないと、コンパイラはエラーを出力します。
enum SomeError: Error {
case unexpected(String)
}
func method() throws -> Void {
throw SomeError.unexpected("予期せぬエラーです")
}
do {
// try キーワードがない
method()
} catch SomeError.unexpected(let error) {
print(error)
}
/* 実行結果 */
// error: call can throw but is not marked with 'try'
// method()
# try!
try!
キーワードを使うと、エラーが発生しなかった場合は関数の戻り値が返されますが、エラーが発生した場合は実行時エラーになります。
次の例では、10以下のときにエラーが発生する関数を定義しています。try!
は明らかにエラーが発生しない状況で使用するのが推奨されています。この場合は引数で100を渡してるのでエラーにならないことが明白です。そのため、try!
で関数を実行し、do-catch文を書かなくても値を取得することができます。
enum SomeError: Error {
case unexpected(String)
}
func method(_ num: Int) throws -> Int {
if num < 10 {
throw SomeError.unexpected("予期せぬエラーです")
}
return num
}
// try! キーワードで実行
// do-catch文を書く必要がない
let result = try! method(100)
result // 100
仮に、10以下の数値を引数に渡すとエラーが投げられるので実行時エラーになります。
enum SomeError: Error {
case unexpected(String)
}
func method(_ num: Int) throws -> Int {
if num < 10 {
throw SomeError.unexpected("予期せぬエラーです")
}
return num
}
let result = try! method(1)
/* 実行結果 */
// Fatal error: 'try!' expression unexpectedly raised an error: __lldb_expr_54.SomeError.unexpected("予期せぬエラーです"):
TIP
try!
はエラーになったときに実行時エラーを起こして、プログラムがストップしてしまいます。むやみやたらに使用すると意図しないエラーを招く可能性があります。「コンパイルエラーが起きているので、なんとなく try!
を書いた」などの使用法は避けた方がいいでしょう。
では、どのような状況で使うのがいいでしょうか。
try!
は明らかにエラーにならない状況で使うのがいいと紹介をしましたが、それ以外にも、プログラムの前提条件として成り立たない場合などに有効です。
例えば、ファイルを読み込んでそのデータをもとに処理をするプログラムがあります。仮にファイルを読み込むのが失敗したら処理はストップしてしまします。
let file = try! loadFile(path: "./file.pdf")
// 次の処理
// 次の処理
// 次の処理
プログラムがストップするのを避けるために、ファイルがなかったら単にreturnすることも可能でしょう。しかし、そのまま処理が進めば他の処理との整合性が合わなくなります。また、ユーザーはそれに気づかずにプログラムを進めてしまう可能性があります。
この場合は、ファイルを読み込む というのは前提条件になります。そのため、ファイルがない状態で処理が進むより、問題を発見した段階でプログラムがストップし、ユーザーにエラーを伝えるほうが適切でしょう。
プログラムの性質によってエラーの対応は異なるので一概には言えませんが、しっかりと try!
の性質を理解し、適切なタイミングで使用するのがいいでしょう。
# try?
try?
キーワードを使うと、エラーが発生したときは nil が返り、エラーが発生しなかったらOptional型の値が返ってきます。
try!
と同様に、do-catch文を書く必要はありません。
enum SomeError: Error {
case unexpected(String)
}
func method(_ num: Int) throws -> Int {
if num < 10 {
throw SomeError.unexpected("予期せぬエラーです")
}
return num
}
// 1以下なのでエラーになるが、nilが返ってくる
let result = try? method(1)
result // nil
try?
は適切に使わないと、エラーを揉み消してしまう性質があるので注意しましょう。
# defer
defer はエラーが発生したかに関わらず必ず実行したい処理がある場合に有効です。do-catch文だと、エラーが発生するとcatch節へ処理が移行します。そのため、実行されるべき処理がスキップされる可能性があります。
例えば、ファイル読み込んだ際に必ずクローズをしなければならないプログラムがあるとします。
enum SomeError: Error {
case unexpected(String)
}
func close() {
print("クローズ")
}
func processFile() throws {
// 処理
// 処理
// 処理
throw SomeError.unexpected("エラー")
}
do {
try processFile()
// エラーが発生するとクローズは実行されない
close()
} catch SomeError.unexpected(let error) {
print(error)
}
/* 実行結果 */
// エラー
しかし、エラーが発生したら close()
は実行されません。catch節のなかでcloseを書くこともできますが、deferを使うと簡単に遅延処理を追加できます。
enum SomeError: Error {
case unexpected(String)
}
func close() {
print("クローズ")
}
func processFile() throws {
// 処理
// 処理
// 処理
throw SomeError.unexpected("エラー")
}
do {
// do節を抜けたら必ず実行される
defer {
close()
}
try processFile()
} catch SomeError.unexpected(let error) {
print(error)
}
/* 実行結果 */
// クローズ
// エラー
do節の中で、deferを実行しています。実行結果からエラーと同時にクローズ処理がされたことが分かります。
# Result型
Result<Success, Failure>型
は、Swift 5から標準ライブラリとして提供されました。標準化の前は、antitypical/Result というライブラリで使われていました。
Result<Success, Failure>型
を使うことで、成功時と失敗時の詳細を明確に表現することができます。
# 定義方法
Result<Success, Failure>型
は型引数に Success
と Failure
をとる列挙型です。Success
には成功したときの型を指定し、Failure
には失敗したときの型を指定します。
public enum Result<Success, Failure> where Failure: Error {
case success(Success)
case failure(Failure)
}
次の例では、画像を読み込む関数 loadImage
を定義し、その戻り値として Result型
を定義しています。画像の読み込みに成功したら Image
型の構造体のインスタンスを返し、失敗したら ImageError
を返しています。
struct Image {
var path: String
}
enum ImageError: Error {
case notFound
case permissionDenied
}
let exists = true
func loadImage(path: String) -> Result<Image, ImageError> {
// 成功したらインスタンスを返す
if exists {
let image = Image(path: path)
return .success(image)
}
// ロードに失敗したらerrorを返す
return .failure(ImageError.notFound)
}
呼び出し後のエラー処理は、switch文を使って返り値を見て判断します。
let result = loadImage(path: "/my/path/image.png")
switch result {
case .success(let image):
print(image)
case .failure(.notFound):
print("NOT FOUND")
case .failure(.permissionDenied):
print("PERMISSION DENIED")
}
/* 実行結果 */
// Image(path: "/my/path/image.png")
do-catch文のように自動的にcatch節に移行するのではなく、関数を呼び出した後に自分でエラー処理をするのが特徴です。
# throwsに置き換える
前節の例は、実は throws
を使ってResult型と同じように書くことができます。
次の例は、前節の例をthrowsを使って書いたものです。throws
でエラーをもつ関数として定義し、内部でエラーを投げています。呼び出し側は、do-catch文で関数を実行しています。
struct Image {
var path: String
}
enum ImageError: Error {
case notFound
case permissionDenied
}
let exists = true
func loadImage(path: String) throws -> Image {
if exists {
return Image(path: path)
}
throw ImageError.notFound
}
// do-catch文で実行する
do {
let image = try loadImage(path: "/my/path/image.png")
print(image)
} catch ImageError.notFound {
print("NOT FOUND")
} catch ImageError.permissionDenied {
print("PERMISSION DENIED")
}
# Result型を使うケース
Result型とdo-catch文/throwsは同じようにエラー処理を書くことができました。しかし、Result型の方がよりエラー情報を正確に型として提供できるケースがあります。
例えば、次のようなユーザー情報を取得する関数があるとします。引数にクロージャを渡して、非同期に実行させます。
func fetchUser(completion: (User) -> Void) {}
これを、do-catch文/throws
でエラー処理を書くと次のようになります。
func fetchUser(completion: (User) -> Void) throws {
// 処理
}
do {
try fetchUser { user in
print(user)
}
} catch {
print(error)
}
しかし、上記のcatch節はfetchUser関数が実行後に同期的に呼び出されるので、非同期処理が終了する前に実行されてしまいます。ユーザー情報の取得が完了する前に呼び出されてしまったら適切なエラー処理をすることができません。
非同期処理が終わった後のエラー処理を書くには、クロージャの引数でエラーを受け取る必要があります。
Optional型にして、成功したパターンと失敗したパターンを書きます。
func fetchUser(completion: (User?, APIError?) -> Void) {}
呼び出し元では、Optional型をアンラップしてエラー処理を書きます。
fetchUser() { user, error in
if let user = user {
print(user)
} else {
if let error = error {
switch error {
case .notFound:
print(error)
}
}
}
}
これでもエラーの処理はできますが型が厳密ではありません。User?
と APIError?
はOptional型なので両方ともnilになる可能性があります。実装をしっかりすればどちらもnilになることはないでしょう。しかし、型としてコンパイラが保証をしてくれているわけではありません。そのため、nilになるかどうかは、関数の実装者に委ねられてしまいます。Swiftの静的型付け言語としてのメリットを活かすには、より厳密に型を定義するべきでしょう。
このようなときに、Result型は正確な型を提供することができます。
成功したときはUserを返し、失敗した時はAPIErrorを返します。
func fetchUser(completion: (Result<User, APIError>) -> Void) {}
fetchUser() { result in
switch result {
case .success(let user):
print(user)
case .failure(.notFound):
print("NOT FOUND")
}
}
また、特にエラーを分別する必要がないとき(成功か失敗かが分かればいいとき)は、Result.get()
と do-catch文
を使うと簡潔に書くことができます。
func fetchUser(completion: (Result<User, APIError>) -> Void) {}
fetchUser() { result in
do {
let user = try result.get()
} catch {
// エラー処理
}
}
上記の例のように、Result型を使用すると型として厳密に定義できるので、不正パターンを考慮する必要がなくなるという利点があります。
# assert
assert
は、Swiftの基本的なデバッグ機能の一つです。assertは、想定外のことが生じたときに、プログラムを終了し、デバッグすることができます。
assert
には条件式を定義します。trueのときは処理が実行され、falseのときは実行時エラーになり、プログラムがストップします。
第一引数に条件式を渡し、第二引数にプログラムが終了したときに出力するメッセージを渡します。
次の例では、10より多い値だとassertにより処理がストップします。
func method(_ arg: Int) {
assert(arg < 10, "プログラムを終了します。")
print("処理を実行する")
}
method(100)
/* 実行結果 */
// Assertion failed: プログラムを終了します。
TIP
基本的に、assertやassertionFailureなどのアサーションは、デバッグ時のみ有効になります。リリース時は無効になります。しかし、これはSwiftのビルド時の最適化オプションで指定することができます。
最適化とは、実行時のパフォーマンス、メモリ量を抑えたりなど調整することです。デバッグ時とリリース時で使い分けて、リリース時にはコードを少なくして、パフォーマンスを最適化しています。その過程でアサーション系のコードは無効になります。
最適化のオプションは主に次のものがあります。
環境 | オプション | 最適化の有無 |
---|---|---|
Debug | -Onone | ✖︎ |
Release | -O | ○ |
以下はそれぞれのビルド実行結果です。Releaseの方は、アサーションがないことが分かります。
Debug:
swiftc -Onone sample1.swift
./sample1
Fatal error: プログラムを終了します。: file swift-test-1.swift, line 2
Release:
swiftc -O sample2.swift
./sample2
// 出力なし
デバッグ時は、想定外のエラーを見つけやすいようにassertを積極的に活用するのがいいでしょう。
# assertionFailure
assertionFailure
は、 assert
と異なり、条件式を与えずに強制的に処理をストップするメソッドです。
引数には、プログラムが終了したときに出力するメッセージを渡します。
func method(_ arg: Int) {
assertionFailure("プログラムを終了します。")
print("処理を実行する")
}
method(100)
/* 実行結果 */
// Fatal error: プログラムを終了します。
# precondition
precondition
は、条件式を定義して、条件が満たされなかった場合に実行時エラーとなります。
次の例では、10より多い値だとpreconditionにより処理がストップします。
func method(_ arg: Int) {
precondition(arg < 10, "プログラムを終了します")
print("処理を実行する")
}
method(100)
/* 実行結果 */
// Precondition failed: プログラムを終了します
# preconditionとassertの違い
assertとpreconditionの動きを見ましたが、両者の使い方は全く同じです。では、この2つの違いは何でしょうか。
assertとpreconditionの違いは、最適化をしたときの挙動 が異なります。
assert の節で、assertはデバッグ時のみ有効と説明をしました。そして、それはSwiftのビルドの最適化レベルによって変わります。
通常、Debugビルドは、-Onone
オプションを使い、Releaseは -O
を使います。assertは -Onone
オプションのみ有効ですが、preconditionは、DebugビルドとReleaseビルドの両方で有効 になります。
種類 | -Onone (Debug) | -O (Release) |
---|---|---|
assert | ○ | ✖︎ |
precondition | ○ | ○ |
# preconditionの用途
preconditionは、Releaseビルドでも使用できるという特徴から、事前の条件を満たすか判定するのに使用します。
例えば、ある関数を実行するときに引数の値が常に必要な場合は、preconditionで処理を制御することができます。
次の例では、配列を受け取って値が常にあることを事前条件とします。
func method(_ arg: [Any]) {}
配列が空の場合は、preconditionでエラーを出すようにします。
func method(_ arg: [Any]) {
precondition(!arg.isEmpty, "プログラムを終了します")
}
この関数に、空の配列を渡すと実行時エラーになり、処理はストップします。
let array = [String]()
method(array)
/* 実行結果 */
// Precondition failed: プログラムを終了します
関数を呼び出すための事前条件を満たすのは、呼び出す側の責務です。関数の責務は、その事前条件が満たされなかった場合にエラーとしてユーザーに伝えて、何が必要か明確にすることです。
preconditionを使用することによって、その明確にするプロセスを定義することができます。
# fetalError
fatalError
はプログラムを強制的に終了させることができます。assertはデバッグ時のみに終了しますが、fatalError
は、デバッグとリリース時の両方で強制的にストップします。
引数には、プログラムが終了したときに出力するメッセージを渡します。
func method() {
fatalError("プログラムを終了します。")
print("処理を実行する")
}
method()
/* 実行結果 */
// Fatal error: プログラムを終了します。
用途としては、想定外の状況になったときや、前提条件として必要な値が渡されてないからプログラムを終了させたいとき、などがあるでしょう。