Error Handling
Swift error handling: throws + do / try / catch + Result. Recoverable vs not, and the conventions that read clean.
Swift — error handling
EXAMPLE
// ===== Define an error type =====
enum AppError: Error {
case notFound
case unauthorized
case validation(String)
case network(URLError)
}
// ===== Throwing function =====
func loadUser(id: Int) throws -> User {
guard id > 0 else { throw AppError.validation("bad id") }
let row = try database.query("...")
guard let row else { throw AppError.notFound }
return User(from: row)
}
// ===== do / try / catch =====
do {
let user = try loadUser(id: 42)
print(user)
} catch AppError.notFound {
print("not found")
} catch AppError.validation(let msg) {
print("invalid: \(msg)")
} catch {
print("other: \(error)")
}
// ===== try? and try! =====
let user: User? = try? loadUser(id: 42) // converts error to nil
let user2: User = try! loadUser(id: 42) // crash on error (use sparingly)
// ===== Result =====
let result: Result<User, AppError> = .success(User(id: 1, name: "Alex"))
switch result {
case .success(let u): print(u)
case .failure(let e): print(e)
}
// Chain via map + flatMap:
let upper = result.map { \$0.name.uppercased() }
// ===== Rethrows (passes errors from closures up) =====
func transform<T>(_ value: T, _ f: (T) throws -> T) rethrows -> T {
return try f(value)
}
// ===== async / await + throws =====
func fetchUser(id: Int) async throws -> User {
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
// Call:
Task {
do {
let user = try await fetchUser(id: 42)
print(user)
} catch {
print(error)
}
}
// ===== Custom error with LocalizedError =====
enum APIError: LocalizedError {
case network
case decoding
case unauthorized
var errorDescription: String? {
switch self {
case .network: return "Network error"
case .decoding: return "Decoding failed"
case .unauthorized: return "Sign in again"
}
}
}
// ===== Re-throwing / wrapping =====
do {
let user = try await fetchUser(id: 42)
} catch let urlError as URLError {
throw AppError.network(urlError) // wrap for callers
} catch {
throw error // re-throw unknown
}
// ===== defer (cleanup that runs even on throw) =====
func process() throws {
let file = try openFile("data.txt")
defer { file.close() }
try parse(file)
}
// ===== When to use what =====
// throws / try: library APIs; recoverable failures with multiple cases
// Result: callback / async results; storing the result
// Optional / nil: simple absence; one failure mode
// fatalError(): programmer errors only; never recoverable
// ===== Patterns to internalise =====
// - Custom Error enum with cases for the failure modes
// - LocalizedError for user-facing messages
// - try? for 'don't care about the error' paths
// - defer for cleanup
// ===== Pitfalls =====
// - try! everywhere -> crashes you would not have with proper catch
// - Catch-all 'catch { }' that swallows errors silently
// - Using Result<T, Error> when a typed enum would be clearer
// - Mixing throws + Result in the same module without a convention
Why it matters
Swift error handling is enum + throws + do / try / catch. Define Error enums per domain, conform to LocalizedError for UI, lean on try? where you do not need the cause, and defer for cleanup. async / await composes cleanly with throws. The patterns are concise once the conventions are in place.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
enum LoginError: Error { case badPassword, locked }
func login(_ pw: String) throws -> String {
if pw == "" { throw LoginError.badPassword }
return "token"
}
do {
let token = try login("hunter2")
print(token)
} catch {
print("failed: \(error)")
}
Try it Yourself »
Discussion
Loading…