Swift: برنامج Swift التعليمي لمعالجة الأخطاء
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
- Define custom error types conforming to the Error protocol
- Declare functions that can throw errors using throws
- Catch and handle different error types with do-catch
- Convert errors to optionals with try? and bypass handling with try!
- Use defer to execute cleanup before a function exits
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:
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
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.
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
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
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
// ============================================
// 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 📖 للعرض فقط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
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)
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
// ============================================
// 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 📖 للعرض فقط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!
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
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
// ============================================
// 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 📖 للعرض فقط=== 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
// ============================================
// 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 📖 للعرض فقط[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
catch FileError.notFound. Use multiple catch clauses for different types. The final catch { } acts as a catch-all.📖 Summary
- The Error protocol is the foundation of Swift error handling; typically define error types as enums
- throws marks a function that may throw; throw actually throws the error
- do-catch catches and handles errors, with support for type-specific handling
- try? converts errors to optionals, returning nil on failure
- try! bypasses error handling and crashes on error — use sparingly
- defer executes cleanup code on function exit, ensuring resources are released
- Good error handling can reduce crash rates from 10%+ to near zero
📝 Exercises
- 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.
- 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.
- 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.