Swift: Swiftのエラー処理チュートリアル:do-catch、try、throwsの解説

エラー処理は車のエアバッグのようなものです——必要にならないことを願いますが、必要な時には命を救います。このレッスンでは、エラーのスローから適切なキャッチまで、Swiftの完全なエラー処理システムを学びます。

1. 学習目標


2. iOS開発者の実話

(1) 課題:設定ファイルがない — アプリが即座にクラッシュ

Aliceは起動時にconfig.jsonファイルを読み込む天気アプリを構築しました。ファイルが存在しないか形式が悪い場合、アプリは完全にクラッシュします:

SWIFT
let configPath = "/app/config.json"
let configData = try! String(contentsOfFile: configPath)
let configLines = configData.split(separator: "\n")

ユーザーCharlieがアプリをアップデートしたところ、クラッシュし続けました——アップデート中に設定ファイルが誤って削除されていたのです。Aliceは50件以上の1つ星レビューを受けました:「開くとすぐクラッシュ——ゴミアプリ!」エラー処理がなければ、1つの例外がユーザー体験全体を台無しにします。

(2) do-catchの解決策

SWIFT
enum ConfigError: Error {
    case fileNotFound
    case invalidFormat
}
func loadConfig(path: String) throws -> [String: String] {
    guard FileManager.default.fileExists(atPath: path) else {
        throw ConfigError.fileNotFound
    }
    return ["theme": "dark", "units": "metric"]
}
do {
    let config = try loadConfig(path: configPath)
    print("Config loaded: \(config)")
} catch ConfigError.fileNotFound {
    print("Config file not found, using defaults")
}

(3) 結果:クラッシュ率が12%から0.1%に

指標 try! 即クラッシュ do-catch で処理
設定ファイルなし アプリクラッシュ デフォルト使用
1つ星レビュー 50+ 0
ユーザー離脱 8% 0.5%
修正の緊急性 即リリース必須 次回アップデートで修正可能

3. Errorプロトコルとエラーのスロー

Swift���Errorプロトコルを使ってエラー型を表現します。Errorに準拠する任意の列挙型、構造体、クラスをエラーとしてスローできます。

100%
graph TB
    A[関数] --> B{エラー?}
    B -->|No| C[値を返す]
    B -->|Yes| D[throw エラー]
    D --> E[呼び出し側がdo-catchでキャッチ]
    E --> F[処理]
    E --> G[伝播]
    E --> H[オプショナルに変換]
キーワード 目的
Error エラープロトコル enum MyError: Error { }
throw エラーをスロー throw MyError.someCase
throws 関数宣言 func foo() throws { }
rethrows エラーをスローする可能性のあるクロージャ func foo(fn: () throws -> Void) rethrows { }

(1) エラー型の定義

SWIFT
enum NetworkError: Error {
    case badURL
    case timeout(seconds: Int)
    case serverError(code: Int)
    case noConnection
}
enum ValidationError: Error {
    case emptyField(fieldName: String)
    case tooShort(minLength: Int)
    case invalidFormat(pattern: String)
}

(2) スロー関数の宣言

SWIFT
enum DivisionError: Error {
    case divisionByZero
}
func divide(_ a: Int, by b: Int) throws -> Int {
    guard b != 0 else {
        throw DivisionError.divisionByZero
    }
    return a / b
}
do {
    let result = try divide(10, by: 0)
    print(result)
} catch {
    print("Error: \(error)")
}

▶ サンプル: ユーザー入力バリデーター

SWIFT
// ============================================
// Validating user input with custom errors
// ============================================
enum ValidationError: Error {
    case emptyField(String)
    case tooShort(field: String, min: Int)
    case invalidEmail
}
func validateRegistration(username: String, email: String, password: String) throws {
    guard !username.isEmpty else {
        throw ValidationError.emptyField("Username")
    }
    guard username.count >= 3 else {
        throw ValidationError.tooShort(field: "Username", min: 3)
    }
    guard !email.isEmpty else {
        throw ValidationError.emptyField("Email")
    }
    guard email.contains("@") else {
        throw ValidationError.invalidEmail
    }
    guard password.count >= 6 else {
        throw ValidationError.tooShort(field: "Password", min: 6)
    }
    print("Validation passed!")
}
do {
    try validateRegistration(username: "Al", email: "alice@test.com", password: "123")
} catch ValidationError.emptyField(let field) {
    print("\(field) cannot be empty")
} catch ValidationError.tooShort(let field, let min) {
    print("\(field) must be at least \(min) characters")
} catch ValidationError.invalidEmail {
    print("Please enter a valid email")
} catch {
    print("Unknown error: \(error)")
}

出力:

TEXT 📖 参照専用
Username must be at least 3 characters

4. do-catchとエラー処理

do-catchはエラーをキャッチして処理する主要なメカニズムです。特定のエラーをキャッチしたり、パターンマッチングを使用したり、エラーを上位に伝播したりできます。

キャッチスタイル 構文 備考
すべてキャッチ catch { } すべてのエラーを処理
特定のエラー catch Error.specific { } その特定のエラーのみ処理
パターンマッチ catch let error as MyError { } 型キャストして処理
条件付き catch where 条件 { } 条件が満たされた場合のみ処理
伝播 try canThrow() 処理せず、スローし続ける

(1) 基本的なdo-catchの使用

SWIFT
enum FileError: Error {
    case notFound, permissionDenied, corrupted
}
func readFile(_ name: String) throws -> String {
    if name == "secret.txt" {
        throw FileError.permissionDenied
    }
    return "File content: \(name)"
}
do {
    let content = try readFile("secret.txt")
    print(content)
} catch FileError.notFound {
    print("File not found")
} catch FileError.permissionDenied {
    print("Access denied")
} catch {
    print("Other error: \(error)")
}

(2) エラー伝播(キャッチしない)

SWIFT
func processFile() throws {
    let content = try readFile("data.txt")
    print("Processing: \(content)")
}
do {
    try processFile()
} catch {
    print("Failed to process: \(error)")
}

▶ サンプル: ファイルの読み取りと解析

SWIFT
// ============================================
// Simulated file reading and parsing with error handling
// ============================================
enum FileParseError: Error {
    case fileNotFound(String)
    case emptyFile
    case invalidFormat(line: Int)
}
func parseConfigFile(path: String) throws -> [String: String] {
    guard path.hasSuffix(".json") else {
        throw FileParseError.fileNotFound(path)
    }
    let content = "name:Alice\nage:25\ninvalid_line"
    let lines = content.split(separator: "\n")
    guard !lines.isEmpty else {
        throw FileParseError.emptyFile
    }
    var config: [String: String] = [:]
    for (index, line) in lines.enumerated() {
        let parts = line.split(separator: ":")
        guard parts.count == 2 else {
            throw FileParseError.invalidFormat(line: index + 1)
        }
        config[String(parts[0])] = String(parts[1])
    }
    return config
}
do {
    let config = try parseConfigFile(path: "app.config")
    print("Config: \(config)")
} catch FileParseError.fileNotFound(let path) {
    print("File not found at: \(path)")
} catch FileParseError.emptyFile {
    print("File is empty")
} catch FileParseError.invalidFormat(let line) {
    print("Invalid format at line \(line)")
} catch {
    print("Unknown error: \(error)")
}

出力:

TEXT 📖 参照専用
File not found at: app.config

5. try?、try!、defer

try?はエラーをオプショナルに変換します。try!はエラーが発生しないことを表明します(発生するとクラッシュ)。deferは関数終了時にクリーンアップコードを実行します。

アプローチ 動作 使用場面
try do-catchが必要 標準的なエラー処理
try? エラー時にnilを返す 成功/失敗のみ気にし、詳細不要
try! エラー時にクラッシュ エラーが絶対にないと確信(テスト/ハードコード��
defer スコープ終了時に実行 リソースクリーンアップ、ファイルクローズ

(1) try?とtry!

SWIFT
enum ParseError: Error {
    case invalidNumber
}
func parseInt(_ text: String) throws -> Int {
    guard let num = Int(text) else {
        throw ParseError.invalidNumber
    }
    return num
}
let result1 = try? parseInt("42")
let result2 = try? parseInt("abc")
print("Result 1: \(result1 ?? 0)")
print("Result 2: \(result2 ?? 0)")
let result3 = try! parseInt("100")
print("Result 3: \(result3)")

(2) deferクリーンアップ

SWIFT
func processResource() {
    print("Step 1: Opening resource")
    defer {
        print("Step 3: Closing resource (always runs)")
    }
    print("Step 2: Using resource")
}
processResource()

▶ サンプル: 安全なネットワークリクエスト処理

SWIFT
// ============================================
// Combining try / try? / try! / defer
// ============================================
enum NetworkError: Error {
    case badURL, noData, timeout
}
func fetchData(from urlString: String) throws -> String {
    defer {
        print("  [cleanup] Closing connection")
    }
    guard !urlString.isEmpty else {
        throw NetworkError.badURL
    }
    guard urlString.contains(".") else {
        throw NetworkError.badURL
    }
    return "{\"name\": \"Alice\", \"age\": 30}"
}
print("=== try? ===")
if let data = try? fetchData(from: "api.example.com") {
    print("Data: \(data)")
} else {
    print("Failed to fetch")
}
print()
print("=== do-catch ===")
do {
    let data = try fetchData(from: "")
    print("Data: \(data)")
} catch NetworkError.badURL {
    print("Error: Invalid URL")
} catch {
    print("Error: \(error)")
}
print()
print("=== try! ===")
let safeData = try! fetchData(from: "api.example.com")
print("Data: \(safeData)")

出力:

TEXT 📖 参照専用
=== try? ===
  [クリーンアップ] 接続を閉じる
データ: {"name": "Alice", "age": 30}

=== do-catch ===
  [クリーンアップ] 接続を閉じる
エラー: 無効なURL

=== try! ===
  [クリーンアップ] 接続を閉じる
データ: {"name": "Alice", "age": 30}

6. 完全な例:設定ファイルローダー

SWIFT
// ============================================
// Configuration file loader
// Combining Error / throws / do-catch / try? / defer
// ============================================
import Foundation
enum ConfigError: Error {
    case fileNotFound(path: String)
    case emptyFile
    case invalidKeyValue(line: Int, content: String)
    case unsupportedKey(String)
}
struct AppConfig {
    var theme: String = "light"
    var fontSize: Int = 14
    var language: String = "en"
    var notifications: Bool = true
}
func loadConfig(from path: String) throws -> AppConfig {
    defer {
        print("[cleanup] Config loader finished")
    }
    guard FileManager.default.fileExists(atPath: path) else {
        throw ConfigError.fileNotFound(path: path)
    }
    let content = try String(contentsOfFile: path)
    let lines = content.split(separator: "\n")
    guard !lines.isEmpty else {
        throw ConfigError.emptyFile
    }
    var config = AppConfig()
    for (index, line) in lines.enumerated() {
        let trimmed = line.trimmingCharacters(in: .whitespaces)
        guard !trimmed.isEmpty, !trimmed.hasPrefix("#") else {
            continue
        }
        let parts = trimmed.split(separator: "=", maxSplits: 1).map(String.init)
        guard parts.count == 2 else {
            throw ConfigError.invalidKeyValue(line: index + 1, content: String(line))
        }
        let key = parts[0].trimmingCharacters(in: .whitespaces)
        let value = parts[1].trimmingCharacters(in: .whitespaces)
        switch key {
        case "theme": config.theme = value
        case "fontSize": config.fontSize = Int(value) ?? config.fontSize
        case "language": config.language = value
        case "notifications": config.notifications = (value == "true")
        default:
            throw ConfigError.unsupportedKey(key)
        }
    }
    return config
}
let tempDir = NSTemporaryDirectory()
let testConfigPath = tempDir + "app.config"
let configContent = """
theme=dark
fontSize=16
language=en
notifications=true
"""
try? configContent.write(toFile: testConfigPath, atomically: true, encoding: .utf8)
do {
    let config = try loadConfig(from: testConfigPath)
    print("=== App Configuration ===")
    print("Theme: \(config.theme)")
    print("Font Size: \(config.fontSize)")
    print("Language: \(config.language)")
    print("Notifications: \(config.notifications)")
} catch ConfigError.fileNotFound(let path) {
    print("Fatal: Config file not found at \(path)")
    print("Using default configuration")
} catch ConfigError.invalidKeyValue(let line, let content) {
    print("Error: Invalid format at line \(line): \(content)")
} catch ConfigError.unsupportedKey(let key) {
    print("Warning: Unsupported key '\(key)', using default")
} catch {
    print("Unexpected error: \(error)")
}
if let fallbackConfig = try? loadConfig(from: tempDir + "nonexistent.config") {
    print("\nLoaded fallback config")
} else {
    print("\nFallback not found, will use defaults")
}

出力:

TEXT 📖 参照専用
[クリーンアップ] 設定ローダー終了
=== アプリ設定 ===
テーマ: dark
フォントサイズ: 16
言語: en
通知: true
[クリーンアップ] 設定ローダー終了

フォールバックが見つかりません、デフォルトを使用します

❓ よくある質問

Q do-catchとtry?の使い分けは?
A 異なるエラー型に対して異なる対応が必要な場合はdo-catchを使用します。成功/失敗のみ気にし、エラーの詳細が不要な場合はtry?(失敗時にnilを返す)を使用します——シンプルでクリーンです。
Q try!は安全ですか?
A いいえ。try!はエラー発生時にクラッシュします——「これは絶対に失敗しないと約束する」とコンパイラに伝えるようなものです。エラーが絶対に発生しないと100%確信できる場合(例:ハードコードされたテストデータ)のみ使用してください。
Q 関数が複数のエラー型をスローする可能性がある場合、catchでどう区別しますか?
A catchの後に具体的なエラー型を指定します(例:catch FileError.notFound)。異なる型用に複数のcatch句を使用します。最後のcatch { }がキャッチオールとして機能します。
Q deferは正確にいつ実行されますか?
A deferは現在のスコープが終了するときに、正常終了でもエラースローでも実行されます。複数のdeferブロックは登録の逆順で実行されます(LIFOスタック)。
Q throwsとrethrowsの違いは?
A throwsは関数自体がスローする可能性があることを意味します。rethrowsは関数自体はスローしませんが、そのクロージャパラメータがスローする可能性があることを意味します。rethrowsはmapのような高階関数でよく使われます。
Q エラー型は列挙型でなければなりませんか?
A いいえ。Errorに準拠する任意の型(列挙型、構造体、クラス)が使用できます。列挙型が最も一般的で、すべてのエラーケースを明確に列挙できるためです。

📖 まとめ


📝 練習問題

  1. 初級: BankError列挙型(insufficientFunds、invalidAccount、frozenAccountのケース)を定義してください。適切なエラーをスローするシミュレーションのwithdraw(amount: Double)関数を作成し、do-catchで処理してください。
  2. 中級: JSON解析関数parsePerson(json: String) throws -> (name: String, age: Int)を作成し、空文字列、nameフィールド欠落、ageが数値でないの3つ��エラーケースを処理してください。try?で1回、do-catchで1回呼び出してください。
  3. 上級: 「コマンドライン電卓」を構築してください。「3 + 4」、「10 / 0」、「abc」のような入力文字列を受け取り、解析して評価します。CalculationError(invalidExpression、divisionByZero、unknownOperatorのケース)を定義し、完全なエラー処理フローを実装してください。do-catchを使ってフレンドリーなエラーメッセージを提供します。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%