How does error handling work in Swift?
TL;DR
A function that can fail is marked throws. It signals failure with throw.
The caller must write try. Handle the error with do/catch, turn it into an optional with try?, or crash with try!.
Errors are values that conform to Error, usually enums. Swift 6 adds typed throws: throws(MyError).
Define and throw an error
enum LoginError: Error {
case emptyEmail
case wrongPassword
case locked(minutes: Int)
}
func login(email: String, password: String) throws -> User {
guard !email.isEmpty else { throw LoginError.emptyEmail }
guard password == "secret" else { throw LoginError.wrongPassword }
return User(email: email)
}Catch with do/catch
do {
let user = try login(email: "a@b.com", password: "123")
print("Hi \(user.email)")
} catch LoginError.locked(let minutes) {
print("Try again in \(minutes) min")
} catch LoginError.wrongPassword {
print("Wrong password")
} catch {
print("Other error: \(error)") // `error` is available here
}Catch clauses are checked top to bottom. The last plain catch handles everything else.
Three ways to use try
| Form | Result | Use when |
|---|---|---|
try | Passes the error up or into catch | You want to handle or forward it |
try? | Optional, nil on error | You do not care why it failed |
try! | Crashes on error | Failure would be a programmer bug |
let user = try? login(email: "", password: "") // nil
let data = try! Data(contentsOf: bundledFileURL) // file is in the app bundleTyped throws (Swift 6)
Before Swift 6, every throwing function threw any Error. Now you can name the type.
func validate(age: Int) throws(ValidationError) -> Int {
guard age >= 0 else { throw .negative }
return age
}
enum ValidationError: Error { case negative }
do throws(ValidationError) {
_ = try validate(age: -1)
} catch {
// error is ValidationError, not any Error
switch error {
case .negative: print("Age cannot be negative")
}
}Use typed throws when the set of errors is fixed and the caller handles each one, for example inside one module. For public APIs, plain throws is often better. It lets you add new errors later without breaking callers.
rethrows
A function that only throws when its closure throws.
func retry<T>(times: Int, _ work: () throws -> T) rethrows -> T {
for _ in 1..<times {
if let value = try? work() { return value }
}
return try work()
}map uses rethrows. That is why [1, 2].map { $0 * 2 } needs no try. With typed throws you can also write throws(E) with a generic E.
defer for cleanup
defer runs when the scope exits, by success or by error.
func readFile() throws -> String {
let handle = try open()
defer { handle.close() } // always runs
return try handle.readAll()
}Errors in async code
func fetchProfile() async throws -> Profile {
let (data, _) = try await URLSession.shared.data(from: profileURL)
return try JSONDecoder().decode(Profile.self, from: data)
}Write try await, in this order.
Result type
Result<Success, Failure> stores an outcome as a value. It was the main tool with completion handlers.
func load(completion: @escaping (Result<User, LoginError>) -> Void) { }
let result = Result { try login(email: "a@b.com", password: "secret") }
let user = try result.get()With async/await, you need Result less often. It is still useful to store an outcome for later.
User-facing messages
Conform to LocalizedError to give a readable message.
extension LoginError: LocalizedError {
var errorDescription: String? {
switch self {
case .emptyEmail: "Please enter your email."
case .wrongPassword: "The password is wrong."
case .locked(let m): "Locked for \(m) minutes."
}
}
}Common follow-up questions
- Is throwing in Swift expensive like exceptions in other languages? No. A throw is like a normal return with an error value. There is no stack unwinding cost.
- Can a non-throwing function call a throwing one? Only inside
do/catch, or withtry?/try!. - What is
throws(Never)? A function that cannot throw. It is the same as a non-throwing function, useful in generic code. - What happens if a
Taskthrows and nobody awaits it? The error is stored in the task and is lost if no one readstask.value. fatalErrorvsthrow?fatalErrorstops the app for impossible states.throwis for expected failures the caller can handle.