ローソンデジタルイノベーション テックブログ

ローソンデジタルイノベーション(LDI)の技術ブログです

Swift 6のTyped throwsを活用する型安全なエラーハンドリング

はじめに

iOSエンジニアの申です。今回はエラーハンドリング時に発生した問題とTyped throwsの活用についてご紹介させていただきます。

発生した問題とその原因

エラーが返される可能性がある関数を実行した際にエラーが発生した場合、そのエラーをハンドリングして内容を表示するところでエラー内容が正しく表示されない問題が発生しました。

class MyViewController: UIViewController {
    func handleResponse(result: MyResult?, error: Error?) {
        if let error = error as? CustomError {
            showError(error)  // 期待している通常のエラー処理
        } else if let error = error {
            showError(CommonError.unknown(error))  // 想定外の不明なエラー処理
        } else if let result = result {
            processResult(result)
        }
    }
}

上記は問題が発生したソースをイメージ化したものになります。エラーの内容を表示するshowErrorメソッドにCustomError型のエラーを渡してエラー内容を表示していますが、なぜかerrorの値が設定されている(nilではない)にもかかわらず期待している通常のエラー内容が表示されない状態でした。

この周りを調べた結果、handleResponseに渡されるエラーはCustomError型には変換できずMyError型にしか変換できないエラーでしたのでCustomError型への変換が常に失敗していたのが原因でした。

let customError = error as? CustomError   // 常にnil
let myError = error as? MyError   // MyError型への変換成功

このようなミスはヒューマンエラーによりいつでも発生する可能性がありますが、Swift 6のTyped throwsが防止策の1つになるのではと考えました。

Typed throwsとは?

Swift 6で追加されたTyped throwsは、エラータイプを明示的に指定して投げるようにすることができます。これによりエラーを受け取る側で型変換を行う必要もなくなり、他の型に誤って変換しようとするとコンパイラが検出してアラートを表示します。

enum MyError: Error {
    case invalidNumber
}

enum CustomError: Error {
    case invalidString
}

func convertToInt(_ input: String) throws(MyError) -> Int {  // throwsの右側にエラータイプを指定
    guard let number = Int(input) else {
        throw MyError.invalidNumber
    }
    return number
}

do {
    let result = try convertToInt("abc")
} catch {
    //let myError = error as? MyError  ←受け取る前からMyErrorタイプになっているため、型変換が不要になる
    let customError = error as? CustomError  // ⚠️Cast from 'MyError' to unrelated type 'CustomError' always fails
}

エラーのハンドリング側で型変換が不要になるためソースコードも簡潔になり、エラータイプの変換ミスも防げる効果があります。

doにエラーのタイプを指定することで、catch側でany Errorタイプにならないようにする方法もあります。

enum MyError: Error {
    case invalidNumber
}

enum NetworkError: Error {
    case timeout
    case invalidResponse
}

func fetchData() throws(NetworkError) {
    throw NetworkError.timeout
}

func convertToInt(_ input: String) throws(MyError) -> Int {
    guard let number = Int(input) else {
        throw MyError.invalidNumber
    }
    return number
}

do throws(MyError) {  // doの右側にエラータイプを指定
    let result = try convertToInt("abc")
    try fetchData()  // 🚨Thrown expression type 'NetworkError' cannot be converted to error type 'MyError'
} catch {
    handleError(error)
}

上記のように、doで指定されたエラー以外のエラーを返すメソッドを使用しようとするとコンパイルエラーが発生することになり、想定外のエラータイプハンドリング発生を防止できるメリットがあります。ただし、エラーのタイプを指定することによるデメリットも存在するため注意が必要です。

注意点

拡張性の制限

Typed throwsを使用すると、関数が投げるエラータイプが固定されるため、新しいエラータイプを追加する際にはAPIの変更が必要です。

多態性(ポリモーフィズム)の制限

Typed throwsは特定のタイプを強制するため、複数の異なるエラータイプを投げる共通のインターフェースを作ることが難しくなります。

最後に

Swift 6のTyped throwsはエラータイプを明確に指定することができ、タイプの安定性を高め、APIを明確に設計することができます。 特に、特定のエラータイプのみを投げる関数では非常に有用です。

Typed throwsの使用を検討するケースは以下にまとめられると思います。

  • 明確なエラータイプが必要な場合(例:JSONパースなど)
  • 他のタイプのエラーを投げる必要がない場合
  • コードの拡張可能性が低く、APIの明確性が重要な場合

しかし、拡張性や多態性(ポリモーフィズム)などの問題により、一般的な関数では依然としてUntyped throwsが推奨されます。様々なエラータイプを投げる可能性がある場合は、Untyped throwsを維持することがより良い選択になり得ます。

今後もローソンデジタルイノベーションでは技術ブログを更新していきますので、是非「読者になる」で応援していただけますと幸いです。