Swift handles recoverable failures with typed errors you throw and catch. Learn throwing functions, do/try/catch, the try? and try! variants, and modeling errors as enums.
Swift models recoverable failures with the Error protocol: a function marked throws can throw, and callers use do/try/catch to handle it. The try keyword at each throwing call makes error propagation explicit and visible in the code, unlike hidden exceptions. Catching specific error types lets you respond appropriately to each failure.
enum LoginError: Error {
case emptyPassword
case tooShort(minimum: Int)
}
func validate(_ password: String) throws {
if password.isEmpty { throw LoginError.emptyPassword }
if password.count < 8 { throw LoginError.tooShort(minimum: 8) }
}
do {
try validate("123") // 'try' marks the throwing call
} catch LoginError.tooShort(let min) {
print("Need at least \(min) chars") // Need at least 8 chars
} catch {
print("Other error: \(error)")
}Beyond do/catch, try? converts a throwing call into an optional (nil on failure), handy when you only care whether it worked. try! force-runs it and crashes on error — reserve it for calls that cannot fail. And defer schedules cleanup to run when the current scope exits, no matter how, guaranteeing resources are released.
// try? -> Optional result; nil if it threw
let number = try? Int("42").map { $0 * 2 } // simplified; nil on failure
// try! -> crashes on error; only when failure is truly impossible
// let must = try! validate("longenough")
func readFile() throws -> String {
let handle = open()
defer { handle.close() } // ALWAYS runs on scope exit (even on throw)
return try handle.readAll()
}
// 'defer' guarantees cleanup — the equivalent of finally.Result<Success, Failure> is an enum that captures either a value or an error as a single return value — useful for asynchronous callbacks (before async/await) and for storing an outcome to handle later. It makes success and failure explicit in the type and pairs naturally with a switch. Modern async code often uses throwing async functions instead, but Result still appears widely.
enum FetchError: Error { case notFound }
func fetch(id: Int) -> Result<String, FetchError> {
id == 1 ? .success("Ada") : .failure(.notFound)
}
switch fetch(id: 1) {
case .success(let name): print("Got \(name)") // Got Ada
case .failure(let err): print("Error \(err)")
}
// Result stores an outcome as ONE value — great for callbacks + deferred handling.
// modern code often prefers 'async throws' functions (see concurrency lesson).