🍑

Combineでエラーを扱う方法まとめ

2021/03/20に公開

Combineでエラーを扱うことを軸にまとめました。

以下のコードがある前提で話を進めます。

combine_error_playground.swift
import Foundation
import Combine

var cancellables = Set<AnyCancellable>()

// 1を足す関数のError
enum AddOneError: Error {
    case error
}

// 1を足すPublisherを返す関数
func addOnePublisher(_ n: Int) -> AnyPublisher<Int, AddOneError> {
    Deferred {
        Future { (promise) in
            guard n < 10 else {
                promise(.failure(AddOneError.error))
                return
            }
            promise(.success(n + 1))
        }
    }
    .eraseToAnyPublisher()
}

sink エラーを受け取る

エラー専用の機能ではないCombineのいつものあれ。
エラーの場合はcompletionでエラーを受け取ります。
エラーを受け取り何をするかはご自由に。

combine_error_playground.swift
// sinkでエラーを処理する
addOnePublisher(10)
    .sink { (completion) in
        switch completion {
        case .finished:
            break
        case .failure(let error): // AnyPublisher<Int, AddOneError>なのでAddOneError型になる
            print(error)
        }
    } receiveValue: { (n) in
        print(n)
    }
    .store(in: &cancellables)

replaceError エラーを正常な値に置き換える

エラーが発生した場合、そのエラーはなかったこととし正常な値(Publisher.Output)に置き換えます。
似たような機能に、nilを置き換えるreplaceNilや、空を置き換えるreplaceEmptyがあります。

combine_error_playground.swift
// Error -> Output
addOnePublisher(10)
    .replaceError(with: 1) // エラーが発生したらOutput型の1に置き換える
    .sink { (completion) in
        switch completion {
        case .finished:
            break
        case .failure(let error):
            print(error)
        }
    } receiveValue: { (n) in
        print(n)
    }
    .store(in: &cancellables)

mapError エラーを別のエラーに変える

エラーを別のエラー型に変換するmap。
Error以外のものには変換できません。

combine_error_playground.swift
// Error -> Error
enum SomeError: Error {
    case error(Error)
}
addOnePublisher(10)
    .mapError { error in // AddOneError を受け取る
        SomeError.error(error) // SomeError に変換する
    }
    .sink { (completion) in
        switch completion {
        case .finished:
            break
        case .failure(let error):
            print(error)
        }
    } receiveValue: { (n) in
        print(n)
    }
    .store(in: &cancellables)

catch エラーをPublisherに置き換える

エラーが発生した場合、Publisherに置き換えます。
Publisherに置き換えるので 割となんでもあり です。

combine_error_playground.swift
// Error -> Publisher(正常な値)
addOnePublisher(10)
    .catch { error in
        // Output型の1を包んだPublisherに置き換える
        Just(1)
    }
    .sink { (completion) in
        switch completion {
        case .finished:
            break
        case .failure(let error):
            print(error)
        }
    } receiveValue: { (n) in
        print(n)
    }
    .store(in: &cancellables)
combine_error_playground.swift
// Error -> Publisher(別のエラー)
addOnePublisher(10)
    .catch { error in
        // 別のErrorを包んだPublisherに置き換える
        Fail(outputType: Int.self, failure: SomeError.error(error))
    }
    .sink { (completion) in
        switch completion {
        case .finished:
            break
        case .failure(let error):
            print(error)
        }
    } receiveValue: { (n) in
        print(n)
    }
    .store(in: &cancellables)
combine_error_playground.swift
// Error -> Publisher(空)
addOnePublisher(10)
    .catch { error in
        // 空のPublisherに置き換え何もなかったことにする
        Empty(outputType: Int.self, failureType: Error.self)
    }
    .sink { (completion) in
        switch completion {
        case .finished:
            break
        case .failure(let error):
            print(error)
        }
    } receiveValue: { (n) in
        print(n)
    }
    .store(in: &cancellables)

handleEvents エラーから副作用

なんでも確認できるhandleEvents。
もちろんErrorも確認できます。
処理が終わった時のエラー処理(sink)とは区別してエラーを扱いたいとき用でしょうか。

combine_error_playground.swift
// Error なら 副作用(何かする)
addOnePublisher(10)
    .handleEvents(receiveCompletion: { (completion) in
        if case .failure(let error) = completion {
            // エラーの場合に何かする
            print(error)
        }
    })
    .sink { (completion) in
        switch completion {
        case .finished:
            break
        case .failure(let error):
            print(error)
        }
    } receiveValue: { (n) in
        print(n)
    }
    .store(in: &cancellables)

retry 再実行する

エラーなら再実行します。
.retry(3) であれば最大3回リトライします。
つまり最初の1回と合わせて最大4回実行されます。
それでもエラーになった場合はエラーとして続きの処理に移ります。

combine_error_playground.swift
// Error ならリトライ
addOnePublisher(10)
    .handleEvents(receiveCompletion: { (completion) in
        print(completion) // リトライするたびにエラーが流れてくることがわかる
    })
    .retry(3) // 3回再実行する
    .sink { (completion) in
        switch completion {
        case .finished:
            break
        case .failure(let error):
            print(error) // 4回実行してもダメならここにくる
        }
    } receiveValue: { (n) in
        print(n)
    }
    .store(in: &cancellables)

assertNoFailure エラーなら処理を止める

エラーなら処理を止めます。
debug/realeaseビルドのどちらでも処理が止まるようです。

combine_error_playground.swift
// Error なら assert
addOnePublisher(10)
    .assertNoFailure() // ここで処理が止まる
    .sink { (completion) in
        switch completion {
        case .finished:
            break
        case .failure(let error):
            print(error)
        }
    } receiveValue: { (n) in
        print(n)
    }
    .store(in: &cancellables)

breakpointOnError エラーならデバッガーで処理を停止する

エラーならデバッガーで処理を停止します。

実際に試したところ、処理は一時停止したけどどのbreakpointOnError()で止まったのかよくわからず使いにくかったです。使い方が悪いのかな。

combine_error_playground.swift
// Error なら breakpoint
addOnePublisher(10)
    .breakpointOnError()
    .sink { (completion) in
        switch completion {
        case .finished:
            break
        case .failure(let error):
            print(error)
        }
    } receiveValue: { (n) in
        print(n)
    }
    .store(in: &cancellables)

breakpointを使うと、それ以外の時も止めたり、特定のエラーのみ止めたりできます。

combine_error_playground.swift
// Error なら breakpoint
addOnePublisher(10)
    // 以下の例の場合、breakpointOnErrorと同等になる
    .breakpoint(receiveSubscription: nil, receiveOutput: nil, receiveCompletion: { (completion) -> Bool in
        if case .failure(_) = completion {
            return true
        } else {
            return false
        }
    })
    .sink { (completion) in
        switch completion {
        case .finished:
            break
        case .failure(let error):
            print(error)
        }
    } receiveValue: { (n) in
        print(n)
    }
    .store(in: &cancellables)

setFailureType(to:) エラーが起きるかもしれないことにする

Failure = Neverの場合絶対にエラーは起きません。しかし続きの処理ではエラーが起きるかもしれない場合に、Failureの型を揃えるために使用します。
iOS14/macOS11ではこの変換は自動で行われるようで、iOS13/macOS10.15をサポートするとき必要になります。

combine_error_playground.swift
// Never なら Error が起きるかもしれないものとする
// この型変換はiOS14/macOS11なら自動で行われる
Timer.publish(every: 1, on: .main, in: .common) // Neverなのでエラーは発生しない
    .autoconnect()
    .setFailureType(to: AddOneError.self) // AddOneErrorが発生する可能性があるものとする。
    .flatMap { _ in
        addOnePublisher(10) // AddOneErrorが発生する可能性がある
    }
    .sink { (completion) in
        switch completion {
        case .finished:
            break
        case .failure(let error):
            print(error)
        }
    } receiveValue: { (n) in
        print(n)
    }
    .store(in: &cancellables)

エラーを発生させる

mapに対するtryMapなど一部オペレータにはtryバージョンが存在します。
tryバージョンではクロージャー内でthrow Errorでき、エラーを発生させることができます。
FailureはError型となります。

combine_error_playground.swift
// 何か -> Error
addOnePublisher(1)
    .tryMap { _ in
        throw AddOneError.error // Error型のエラーとして処理される
    }
    .sink { (completion) in
        switch completion {
        case .finished:
            break
        case .failure(let error):
            print(error)
        }
    } receiveValue: { (n) in
        print(n)
    }
    .store(in: &cancellables)

addOnePublisherで使用しているFutureもエラーを発生させることができます。

combine_error_playground.swift
Future<Int, AddOneError> { (promise) in
    promise(.failure(AddOneError.error)) // AddOneError型のエラーとして処理される
}
.sink { (completion) in
    switch completion {
    case .finished:
        break
    case .failure(let error):
        print(error)
    }
} receiveValue: { (n) in
    print(n)
}
.store(in: &cancellables)

flatMapのようにPublisherを返す場合、tryバージョンはありませんがFailを使ってエラーを発生させることができます。

combine_error_playground.swift
addOnePublisher(1)
    .flatMap { _ in
        Fail(outputType: Int.self, failure: AddOneError.error)
    }
    .sink { (completion) in
        switch completion {
        case .finished:
            break
        case .failure(let error):
            print(error)
        }
    } receiveValue: { (n) in
        print(n)
    }
    .store(in: &cancellables)

すぐ上でも使用しているFailもエラーを発生させることができます。エラーそのものと考えた方がいいでしょうか。

combine_error_playground.swift
Fail(outputType: Int.self, failure: AddOneError.error)
    .sink { (completion) in
        switch completion {
        case .finished:
            break
        case .failure(let error):
            print(error)
        }
    } receiveValue: { (n) in
        print(n)
    }
    .store(in: &cancellables)

Discussion