Crash Courses · PhD
Error Handling & the Result Type
Handle errors the Swift way with throwing functions, do/try/catch, defer, and the Result<Success, Failure> type for explicit error propagation.
20 min read+185 XP on completionCert: Swift Crash Course
Tap any word in the text below to start reading from there.
Error Handling & the Result Type
Defining Error Types
// Swift errors conform to the Error protocol (usually enums)
enum ValidationError: Error, LocalizedError {
case emptyField(fieldName: String)
case tooShort(fieldName: String, minimum: Int, actual: Int)
case invalidEmail(String)
case outOfRange(value: Double, min: Double, max: Double)
var errorDescription: String? {
switch self {
case .emptyField(let name):
return "\(name) cannot be empty"
case .tooShort(let name, let min, let actual):
return "\(name) must be at least \(min) characters (got \(actual))"
case .invalidEmail(let email):
return "'\(email)' is not a valid email address"
case .outOfRange(let value, let min, let max):
return "\(value) must be between \(min) and \(max)"
}
}
}---
Throwing Functions
func validateEmail(_ email: String) throws -> String {
guard !email.isEmpty else {
throw ValidationError.emptyField(fieldName: "Email")
}
guard email.contains("@") && email.contains(".") else {
throw ValidationError.invalidEmail(email)
}
return email.lowercased().trimmingCharacters(in: .whitespaces)
}
func validatePassword(_ password: String) throws -> String {
guard !password.isEmpty else {
throw ValidationError.emptyField(fieldName: "Password")
}
guard password.count >= 8 else {
throw ValidationError.tooShort(fieldName: "Password", minimum: 8, actual: password.count)
}
return password
}
// Calling throwing functions
do {
let email = try validateEmail("jordan@jst.com")
let password = try validatePassword("secure123")
print("Valid: \(email), password length: \(password.count)")
} catch ValidationError.emptyField(let name) {
print("Empty: \(name)")
} catch ValidationError.invalidEmail(let e) {
print("Bad email: \(e)")
} catch ValidationError.tooShort(let name, let min, let actual) {
print("\(name) too short: \(actual) < \(min)")
} catch {
print("Unexpected error: \(error)") // catch-all
}---
try?, try!, and rethrows
// try? — returns Optional, nil on error
let validEmail = try? validateEmail("alice@example.com") // Optional("alice@example.com")
let invalidEmail = try? validateEmail("notanemail") // nil
// Combine with ?? for defaults
let email = (try? validateEmail(userInput)) ?? "unknown@email.com"
// try! — only use when error is truly impossible
// e.g. a hardcoded regex pattern that you KNOW is valid
let emailRegex = try! NSRegularExpression(pattern: "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}")
// rethrows — for higher-order functions that throw only if the closure throws
func transform<T, U>(_ value: T, using fn: (T) throws -> U) rethrows -> U {
return try fn(value)
}
let result = try transform("jordan@jst.com", using: validateEmail)---
defer
func processFile(named filename: String) throws -> String {
print("Opening \(filename)")
defer {
print("Closing \(filename)") // always runs when function exits
}
guard filename.hasSuffix(".txt") else {
throw ValidationError.invalidEmail(filename) // reusing for demo
}
// ... process the file ...
return "Processed: \(filename)"
}
// Even when an error is thrown, defer runs:
do {
let result = try processFile(named: "data.csv")
print(result)
} catch {
print("Error: \(error)")
}
// Output:
// Opening data.csv
// Closing data.csv ← defer ran even though error was thrown
// Error: ...---
Result<Success, Failure>
// Use Result when you want to return success/failure without throwing
// Great for async callbacks and when you want to store the result
enum NetworkError: Error {
case noConnection
case timeout
case invalidResponse(statusCode: Int)
case decodingFailed(Error)
}
func fetchUser(id: Int, completion: @escaping (Result<User, NetworkError>) -> Void) {
// Simulate network call
if id <= 0 {
completion(.failure(.invalidResponse(statusCode: 400)))
return
}
let user = User(id: String(id), name: "User \(id)")
completion(.success(user))
}
fetchUser(id: 1) { result in
switch result {
case .success(let user):
print("Found: \(user.name)")
case .failure(let error):
print("Error: \(error)")
}
}
// Result with map/flatMap for chaining
let rawInput = " jordan@jst.com "
let validated: Result<String, ValidationError> = Result {
try validateEmail(rawInput)
}
let upper = validated.map { $0.uppercased() }
switch upper {
case .success(let email): print("Validated: \(email)")
case .failure(let error): print("Failed: \(error.errorDescription ?? "")")
}
// get() throws for interop
let email2 = try? validated.get() // returns Optional<String>Reading progress
0% read
In this module
throws
do/try/catch
try?
try!
0%