Swiftでdeferの中のエラーハンドリングを試みる

Last Updated on 2024年2月15日 by lemonade

deferはSwiftでコードブロックを抜ける際に必ず行う処理として定義できるものです。ファイルのcloseなどクリーンアップ処理に使われることがほとんどです。

しかし、クリーンアップ処理最中に投げられたエラーを伝播させたい時はありませんか?

この記事ではSwiftのdeferでthrowされた場合の方法を模索してみます。

定義

まず、エラーを起こせる対象のファイルクラスを作成します。

struct File {
    var onOpen = false
    var onClose = false
    var onWrite = false
    var onRead = false
    
    func open() throws {
        if onOpen { throw Error.open }
    }
    func close() throws {
        if onClose { throw Error.close }
    }
    func write(text: String) throws {
        if onWrite { throw Error.write }
    }
    func read() throws -> String {
        if onRead { throw Error.read }
        return "Hello, world!"
    }
    
    enum Error: Swift.Error {
        case open, close, write, read
    }
}

そしてこれを以下のように使ってみます。

func 処理(file: File) throws -> String {
    // ファイルを開く
    try file.open()
    
    // スコープから出る際にファイルを閉じる
    defer { try file.close() } // Call can throw, but errors cannot be thrown out of a defer body
    
    // ファイルを読み込む
    let text = try file.read()
    
    // ファイルに書き込む
    try file.write(text: text)
    
    // ファイルの内容を返す
    return text
}

もちろんdeferの中ではthrowをそのままにできないのでエラーが出て実行できません。

方法1 try!で揉み消す

func 処理(file: File) throws -> String {
    try file.open()
    
    defer { try! file.close() }
    
    let text = try file.read()
    try file.write(text: text)
    return text
}

print(try 処理(file: File())) // Hello, world!

try!だと強制的にthrowされないと示しているためthrowされたことにならずコンパイルが通ります。

print(try 処理(file: File(onClose: true)))
// Thread 1: Fatal error: 'try!' expression unexpectedly raised an error: File.Error.close

しかし、もしcloseでエラーが投げられた場合fatalErrorが発生しプログラムが落ちてしまいます。これはcatch後にfatalErrorを投げる場合でも同じです

方法2 キャッチしてログだけ出す。

func 処理(file: File) throws -> String {
    try file.open()
    
    defer {
        do {
            try file.close()
        } catch {
            print(error)
        }
    }
    
    let text = try file.read()
    try file.write(text: text)
    return text
}

print(try 処理(file: File(onClose: true)))
// close
// Hello, world!

これだと、closeされた際にエラーが出たことは気づけますが、そのまま後の処理が続きます。クリーンアップ処理が完了しなくても一旦、後の処理に問題がないのであればこれでいいでしょう。

方法3 inoutでResult型でエラーを返す

defer内では引数は扱うことができるので、inoutでResult型を受け取ることでdefer内で起こったErrorを外に伝えることができます。

func 処理(file: File, result: inout Result<String, any Error>) {
    do {
        try file.open()
        
        defer {
            do {
                try file.close()
            } catch {
                result = .failure(error)
            }
        }
        
        let text = try file.read()
        try file.write(text: text)
        
        result = .success(text)
    } catch {
        result = .failure(error)
    }
}

var result: Result<String, any Error> = .success("")
処理(file: File(onClose: true), result: &result)
print(result) // failure(File.Error.close)

しかし、Errorが2つ起こった場合はどうなるのでしょうか

var result: Result<String, any Error> = .success("")
処理(file: File(onClose: true, onWrite: true), result: &result)
print(result) // failure(File.Error.write)

writeとcloseでエラーが発生した場合、write→close→Error.closeのcatch→Error.writeのcatchという順で起こるため、close時のエラーはwrite時のエラーに上書きされてしまいました。一律でエラーが出たときのハンドリング方法があるのならこれでいいですが、細かく分ける必要がある場合はこれではいけないかもしれません。

方法4 inoutでResultの配列を受け取る

[Error]型はErrorに準拠していないのでextensionします。

extension [any Error]: Error {}

func 処理(file: File, result: inout Result<String, [any Error]>) {
    do {
        try file.open()
        
        defer {
            do {
                try file.close()
            } catch {
                switch result {
                case .success(_):
                    result = .failure([error])
                case .failure(let errors):
                    result = .failure(errors + [error])
                }
            }
        }
        
        let text = try file.read()
        try file.write(text: text)
        
        result = result.map { _ in text }
    } catch {
        switch result {
        case .success(_):
            result = .failure([error])
        case .failure(let errors):
            result = .failure(errors + [error])
        }
    }
}

var result: Result<String, [any Error]> = .success("")
処理(file: File(onClose: true, onWrite: true), result: &result)
print(result) // failure([File.Error.close, File.Error.write])

かなり冗長になってしまいました。

まとめ

とりあえずResultをinoutで扱うことで外にdefer中のエラーは伝えることができました。ただ、冗長になってしまうのでResult.get()でthrowに直すラップ関数を作ったりdoでインラインでget()までしたりしてもいいかもしれません。

Leave a Comment

CAPTCHA