# 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>型 は型引数に SuccessFailure をとる列挙型です。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: プログラムを終了します。

用途としては、想定外の状況になったときや、前提条件として必要な値が渡されてないからプログラムを終了させたいとき、などがあるでしょう。