Swift
swift4

Swift 4.0 エラー処理入門

はじめに

Swiftのエラー処理について初心者ながら記述方法を整理してみます。
本記事はSwift 4.0に対応しています。

エラー処理とは

以下のようにメソッドで何らかのエラーが発生した場合に呼び出し元へエラーとして返すことができます。

// エラーを持つメソッド
func methodA(text: String) throws {
    if text.isEmpty {
        throw NSError(domain: "errorメッセージ", code: -1, userInfo: nil)
    }
    print(text)
}

// 呼び出し元メソッド
func method() {
    do {
        try? methodA(text: "")
    } catch {
        // エラー処理
    }
}

プロトコルError

上記の例ではNSErrorをエラーとしましたが、これはNSErrorもErrorプロトコルに準拠しているためです。Swift 3.0のエラー処理ではErrorプロトコルに準拠した列挙型を定義して処理にあわせたエラーを返すことが一般的のようです。Errorの列挙型のエラー定義ではエラーに引数を持たせて、一緒に渡すことも可能です。
※Swift 2.0まではプロトコルErrorTypeとなっていましたがSwift 3.0からはErrorに変更になりました

enum APIError: Error {
    case network
    case server(Int) // Int型を引数とする
    case unknown(String) // String型を引数とする
}

throw文

定義したエラーは以下のようにエラーを投げることができます。

if 何かの条件 {
    throw APIError.netWork
}

if 何かの条件 {
    throw APIError.unknown("errorメッセージ")
}

throwsキーワード

エラーを持つメソッドはメソッドの定義にthrowsキーワードを書きます。この記述でエラーを持つメソッドであること宣言します。

func methodA() throws -> Void {

    if 何かの条件 {
        throw APIError.netWork
    }

    if 何かの条件 {
        throw APIError.unknown("errorメッセージ")
    }
}

do-catch構文とキーワードtry

エラーを持つメソッドを呼び出す側の構文です。以下のようにエラー処理を行います。do節にエラーを持つメソッドとメソッドの前にキーワードtryを書きます。エラー処理はcatch節に書きます。

do {
    try methodA()
} catch {
    // エラー処理
}

catch節では以下のようにエラーのパターンで分岐することが可能です。引数を持つエラーはletまたはvarで引数を受け取り使用することができます。

do {
    try methodA()
} catch APIError.Network {
    // エラー処理
} catch APIError.Unknown(let message) {
    // エラー処理
    print(message)
} catch {
    // それ以外のエラー
}

複数メソッドをまたがる場合のエラー処理

呼び出したメソッドのさらに先のメソッドがエラーを持っている場合かつ最初の呼び出し元がエラー処理したい場合には以下のように書きます。

func method() {
    do {
        try methodB()
    } catch APIError.network {
        // エラー処理
    } catch APIError.unknown(let message) {
        // エラー処理
        print(message)
    } catch {
        // それ以外のエラー
    }
}

func methodB() throws { // エラーを渡すのでメソッド定義にthrowsが必要
    do {
        try methodA(text: "")
    }
    // catch節を書かなければ呼び出し元へそのままエラーを渡すことができる
}

もちろん、methodB()内でエラー処理するのであれば、以下のように書きますし、呼び出し元のdo-catch構文は不要になります。

func methodB() { // このメソッド内でエラー処理するのでメソッド定義にthrowsは不要
    do {
        try methodA(text: "")
    } catch {
        // エラー処理
    }
}

rethrowsキーワード

メソッドが引数として受け取ったクロージャがエラーを持つ場合は以下のように書けます。

func method1(method: () throws -> Void) {
  do {
    try method()
  } catch {
    // エラー処理
  }
}

rethrowsキーワードは受け取ったクロージャのエラーをメソッドの呼び出し元へそのままエラーを返したい場合に使います。

func method1(method: () throws -> Void) rethrows {
    try method()
}

func medhod2() {
    do {
        try method1(methodC)
    } catch {
        // エラー処理
    }
}

func methodC() throws {
  throw APIError.network
}

なお、rethrowsキーワードとthrowsキーワードの同時使用はできないようです。

キーワードtry?とtry!

エラーを持つメソッドの呼び出し時にはキーワードtry使いましたが、try?とtry!というキーワードもあります。用途は以下の通りです。

キーワードtry?

以下のように記述することでエラーを無視したメソッドの実行が可能です。do-catch構文は必要ありません。

try? methodA()

実際にはオプショナル型が返されるため、エラーが発生した場合はnil、それ以外の場合はオプショナル型の値が帰ってきます。
try?では実際に発生したエラーを黙殺することになるため、tryでエラーハンドリングするかtry!を使用するのが一般的です。

キーワードtry!

エラーが発生しなかった場合には通常の値が返されますが、エラーが発生した場合はクラッシュします。こちらもdo-catch構文は必要ありません。エラーが起こり得ないケースで使用します。

try! methodA()

参考

Swiftの ! は危険信号か?
https://qiita.com/hironytic/items/0ca33b2c0415cdd38aff

「ここでエラーが出るはずはない」というのをちゃんと検討した上で正しく使う ! は、危険信号ではなくて、前提条件を表す強い意思表示になる!

defer文

エラーが発生すると処理を中断してエラーを返して呼び出し元に戻ります。defer文はコードブロック内の処理が最後まで終了するか、処理を中断した場合に何かしらの後処理をしたい場合に使用します。

func method1() {
  do {
    defer { エラーが起きた場合でも必ず実行したい後処理を事前に宣言 }
    try methodA(text: "")
    defer { methodAでエラーが起きた場合は実行されない後処理 }
  } catch {
    // エラー処理
  }
}

複数のdefer文

複数のdefer文を使った場合の実行順序は以下のようになります。

func method1() {
  do {
    defer { 後処理1 } // 次に実行される
    defer { 後処理2 } // 最初に実行される
    try methodA(text: "")
  } catch {
    // エラー処理
  }
}

最後に

認識違いがありましたらアドバイス頂ければ幸いです。ありがとうございました。

謝辞

キーワードtry!およびtry?に関してアドバイスいただきました@koherさんに感謝いたします。

参考文献

以下の技術書を参考しました。

  • 詳解Swift 第4版 - SBクリエイティブ株式会社
  • 詳解Swift 第3版 - SBクリエイティブ株式会社
  • 詳解Swift 改訂版 - SBクリエイティブ株式会社
  • Swift 2 標準ガイドブック - マイナビブックス