Error Handling
Modeling failure explicitly with throwing functions and do/try/catch.
2 min de lecture
Swift handles recoverable errors through a dedicated system built around throw, try, and do/catch — distinct from optionals (for "might have no value") and distinct from crashes (for programmer mistakes that should never happen).
Defining an error type
Errors are typically modeled as an enum conforming to the Error protocol — a natural fit, since a set of related failure cases is exactly what enums are good at describing:
enum ValidationError: Error {
case tooShort
case missingAtSign
}
func validate(email: String) throws {
if email.count < 5 {
throw ValidationError.tooShort
}
if !email.contains("@") {
throw ValidationError.missingAtSign
}
}The throws keyword marks validate(email:) as a throwing function — one that can fail by throwing an error instead of returning normally. Any function that calls it must acknowledge that possibility.
Calling a throwing function: do/try/catch
do {
try validate(email: "a@b")
print("Valid email")
} catch ValidationError.tooShort {
print("Email is too short")
} catch ValidationError.missingAtSign {
print("Email needs an @ symbol")
} catch {
print("Unknown error: \(error)")
}try marks each call that might throw. If it does, control jumps immediately to the matching catch block — the specific cases first, and a catch-all catch (binding the generic error) to cover anything not explicitly matched. Nothing after a thrown try line runs; execution jumps straight to catch.
Alternatives to do/catch
Sometimes you don't want the full do/catch ceremony. try? converts a throwing call into an optional — nil if it threw, the value otherwise:
func parseAge(_ text: String) throws -> Int {
guard let age = Int(text) else { throw ValidationError.tooShort }
return age
}
let age = try? parseAge("thirty") // nil — the error is discarded, not inspectedtry! force-runs a throwing call, crashing if it actually throws — the error-handling equivalent of force-unwrapping an optional with !, and just as risky. Reserve it for cases where failure is genuinely impossible given the surrounding logic, not as a shortcut to avoid writing do/catch.
Propagating errors instead of handling them
A function doesn't have to catch an error it encounters — marking itself throws lets it pass the error up to whatever called it:
func registerUser(email: String) throws {
try validate(email: email) // if this throws, registerUser rethrows automatically
print("User registered")
}This keeps error-handling logic centralized wherever it's most useful to actually decide what to do about a failure, rather than forcing every function in a call chain to handle every possible error itself.