Swift: Swift Error Handling Tutorial

Error handling is like a car's airbag — you hope you never need it, but when you do it can save the day. This lesson covers Swift's complete error handling system, from throwing errors to catching them gracefully.

1. What You'll Learn


2. An iOS Developer's Real Story

(1) Pain: Config file missing — App crashes immediately

Alice built a weather app that reads a config.json file on launch. If the file doesn't exist or has a bad format, the app crashes outright:

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

User Charlie updated the app and it kept crashing — because the config file had been accidentally deleted during the update. Alice got 50+ one-star reviews: "Opens and immediately crashes — trash app!" Without error handling, a single exception can ruin the entire user experience.

(2) The do-catch Solution

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) Result: Crash rate from 12% to 0.1%

Metric try! Crashes Immediately do-catch Handles It
Config missing App crash Use defaults
1-star reviews 50+ 0
User churn 8% 0.5%
Fix urgency Must release immediately Can fix next update

3. The Error Protocol and Throwing Errors

Swift uses the Error protocol to represent error types. Any enum, struct, or class that conforms to Error can be thrown as an error.

100%
graph TB
    A[Function] --> B{Error?}
    B -->|No| C[Return value]
    B -->|Yes| D[throw Error]
    D --> E[Caller catches with do-catch]
    E --> F[Handle]
    E --> G[Propagate]
    E --> H[Convert to Optional]
Keyword Purpose Example
Error Error protocol enum MyError: Error { }
throw Throw an error throw MyError.someCase
throws Function declaration func foo() throws { }
rethrows Closures that may throw func foo(fn: () throws -> Void) rethrows { }

(1) Defining Error Types

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) Declaring Throwing Functions

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)")
}

▶ Example: User Input Validator

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)")
}

Output:

TEXT 📖 Display only
Username must be at least 3 characters

4. do-catch and Error Handling

do-catch is the primary mechanism for catching and handling errors. You can catch specific errors, use pattern matching, or propagate errors upward.

Catch Style Syntax Notes
Catch all catch { } Handles every error
Specific error catch Error.specific { } Only handles that particular error
Pattern match catch let error as MyError { } Type cast and handle
Conditional catch where condition { } Handle only when condition is met
Propagate try canThrow() Don't handle, keep throwing

(1) Basic do-catch Usage

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) Error Propagation (Not Catching)

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

▶ Example: File Reading and Parsing

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)")
}

Output:

TEXT 📖 Display only
File not found at: app.config

5. try?, try!, and defer

try? converts errors to optionals. try! asserts no error will occur (crashes if one does). defer runs cleanup code when a function exits.

Approach Behavior When to Use
try Requires do-catch Standard error handling
try? Returns nil on error Only care about success/failure, not details
try! Crashes on error Certain there will be no error (e.g. test/hardcoded)
defer Runs on scope exit Resource cleanup, closing files

(1) try? and 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 Cleanup

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

▶ Example: Safe Network Request Handling

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)")

Output:

TEXT 📖 Display only
=== try? ===
  [cleanup] Closing connection
Data: {"name": "Alice", "age": 30}

=== do-catch ===
  [cleanup] Closing connection
Error: Invalid URL

=== try! ===
  [cleanup] Closing connection
Data: {"name": "Alice", "age": 30}

6. Full Example: Configuration File Loader

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")
}

Output:

TEXT 📖 Display only
[cleanup] Config loader finished
=== App Configuration ===
Theme: dark
Font Size: 16
Language: en
Notifications: true
[cleanup] Config loader finished

Fallback not found, will use defaults

❓ FAQ

Q How do I choose between do-catch and try??
A Use do-catch when you need to respond differently to different error types. Use try? when you only care about success/failure and don't need error details — it returns nil on failure, clean and simple.
Q Is try! safe?
A No. try! crashes when an error occurs — it's like telling the compiler "I promise this won't fail." Only use it when you're 100% certain an error cannot happen (e.g. hardcoded test data).
Q A function can throw multiple error types — how do I distinguish them in catch?
A Specify the concrete error type after catch, such as catch FileError.notFound. Use multiple catch clauses for different types. The final catch { } acts as a catch-all.
Q When exactly does defer execute?
A defer runs when the current scope exits, whether normally or by throwing an error. Multiple defer blocks execute in reverse registration order (LIFO stack).
Q What's the difference between throws and rethrows?
A throws means the function itself may throw. rethrows means the function itself doesn't throw, but its closure parameter might. rethrows is commonly used with higher-order functions like map.
Q Must error types be enums?
A No. Any type conforming to Error will work — enums, structs, and classes are all fine. Enums are most common because they clearly enumerate all error cases.

📖 Summary


📝 Exercises

  1. Beginner: Define a BankError enum (with cases insufficientFunds, invalidAccount, frozenAccount). Write a simulated withdraw(amount: Double) function that throws the appropriate errors, then handle them with do-catch.
  2. Intermediate: Write a JSON parsing function parsePerson(json: String) throws -> (name: String, age: Int) that handles three error cases: empty string, missing name field, and age not a number. Call it once with try? and once with do-catch.
  3. Challenge: Build a "command-line calculator." Accept input strings like "3 + 4", "10 / 0", "abc", parse and evaluate them. Define a CalculationError (with cases invalidExpression, divisionByZero, unknownOperator) and implement a complete error handling flow. Use do-catch to provide friendly error messages.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏