Swift: Swiftのエラー処理チュートリアル:do-catch、try、throwsの解説
エラー処理は車のエアバッグのようなものです——必要にならないことを願いますが、必要な時には命を救います。このレッスンでは、エラーのスローから適切なキャッチまで、Swiftの完全なエラー処理システムを学びます。
1. 学習目標
- Errorプロトコルに準拠したカスタムエラー型の定義
- throwsを使ったエラーをスローできる関数の宣言
- do-catchを使った異なるエラー型のキャッチと処理
- try?によるエラーのオプショナル変換とtry!による処理のバイパス
- deferを使った関数終了前のクリーンアップ実行
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に準拠する任意の列挙型、構造体、クラスをエラーとしてスローできます。
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に準拠する任意の型(列挙型、構造体、クラス)が使用できます。列挙型が最も一般的で、すべてのエラーケースを明確に列挙できるためです。
📖 まとめ
- ErrorプロトコルがSwiftエラー処理の基盤。通常���エラー型は列挙型で定義
- throwsはスローする可能性のある関数にマークし、throwは実際にエラーをスロー
- do-catchでエラーをキャッチして処理し、型固有の処理をサポート
- try?はエラーをオプショナルに変換し、失敗時にnilを返す
- try!はエラー処理をバイパスし、エラー時にクラッシュ——控えめに使用
- deferは関数終了時にクリーンアップコードを実行し、リソース解放を保証
- 適切なエラー処理はクラッシュ率を10%以上からほぼゼロに削減できる
📝 練習問題
- 初級: BankError列挙型(insufficientFunds、invalidAccount、frozenAccountのケース)を定義してください。適切なエラーをスローするシミュレーションのwithdraw(amount: Double)関数を作成し、do-catchで処理してください。
- 中級: JSON解析関数parsePerson(json: String) throws -> (name: String, age: Int)を作成し、空文字列、nameフィールド欠落、ageが数値でないの3つ��エラーケースを処理してください。try?で1回、do-catchで1回呼び出してください。
- 上級: 「コマンドライン電卓」を構築してください。「3 + 4」、「10 / 0」、「abc」のような入力文字列を受け取り、解析して評価します。CalculationError(invalidExpression、divisionByZero、unknownOperatorのケース)を定義し、完全なエラー処理フローを実装してください。do-catchを使ってフレンドリーなエラーメッセージを提供します。