Error Protocol & Custom Errors
Error Protocol & Custom Errors
Swift uses the Error protocol to represent errors that can occur at runtime. Any type can conform to Error—typically an enum.
The Error Protocol
The Error protocol is empty—it serves as a marker:
public protocol Error : Sendable { }
Any type conforming to Error can be thrown and caught.
Defining Custom Errors
The most common pattern is to use enums with associated values:
enum NetworkError: Error {
case invalidURL
case noConnection
case serverError(statusCode: Int)
case decodingFailed(underlying: Error)
}
Enums are ideal because each case can carry different associated data, providing rich error information.
Simple Errors
For cases without associated values:
enum ValidationError: Error {
case emptyField
case invalidEmail
case passwordTooShort
case ageOutOfRange
}
Error Hierarchies
For complex domains, create error hierarchies:
enum DatabaseError: Error {
case connectionFailed(underlying: Error)
case queryFailed(sql: String, reason: String)
case recordNotFound(id: String)
case constraintViolation(field: String)
}
enum APIError: Error {
case unauthorized
case forbidden
case notFound
case serverError(status: Int, message: String?)
case networkError(underlying: Error)
}
LocalizedError
For user-facing error messages, conform to LocalizedError:
enum AppError: Error, LocalizedError {
case fileNotFound(name: String)
case permissionDenied
var errorDescription: String? {
switch self {
case .fileNotFound(let name):
return "File '\(name)' was not found."
case .permissionDenied:
return "You don't have permission to access this."
}
}
}
print(AppError.fileNotFound(name: "config.json").localizedDescription)
// "File 'config.json' was not found."
Custom Error Properties
You can add computed properties to errors:
enum APIError: Error {
case rateLimited(retryAfter: TimeInterval)
case serverError(code: Int)
var shouldRetry: Bool {
switch self {
case .rateLimited: return true
case .serverError(let code): return code >= 500
}
}
}
Equatable and Hashable Errors
For testing and comparison:
enum TestError: Error, Equatable {
case caseA
case caseB
}
// Automatically Equatable because all cases have no associated values
When associated values are present, you need to manually conform or use the auto-synthesis in Swift 5.1+.
try/catch/throw
try/catch/throw
Throwing Errors
Use throw to signal an error:
func divide(_ a: Int, by b: Int) throws -> Int {
guard b != 0 else {
throw MathError.divisionByZero
}
return a / b
}
enum MathError: Error {
case divisionByZero
}
Any function that can throw must be marked with throws in its signature.
Catching Errors
Use do/try/catch to handle errors:
do {
let result = try divide(10, by: 0)
print(result)
} catch MathError.divisionByZero {
print("Cannot divide by zero!")
} catch {
print("Unknown error: \(error)")
}
Multiple Catch Blocks
do {
let data = try fetchData()
let parsed = try parse(data)
} catch NetworkError.noConnection {
print("No internet")
} catch NetworkError.serverError(let code) {
print("Server error: \(code)")
} catch {
print("Other error: \(error)")
}
The error implicit variable holds the thrown error in the default catch block.
Propagating Errors
Functions can propagate errors to their caller without handling them:
func processFile(named name: String) throws {
let data = try readFile(name) // throws propagate
let parsed = try parse(data) // throws propagate
save(parsed)
}
try? (Optional Try)
Converts errors to optionals—nil if an error occurs:
let result = try? divide(10, by: 0) // nil
let valid = try? divide(10, by: 2) // Optional(5)
This is useful when the error doesn't matter and you just need the value or nil.
try! (Force Try)
Crashes if an error is thrown—use only when you're certain no error will occur:
let config = try! JSONDecoder().decode(Config.self, from: data) // crashes on error
Rethrowing Functions
Functions marked rethrows only throw if their closure argument throws:
func process<T>(_ items: [T], transform: (T) throws -> T) rethrows -> [T] {
var result: [T] = []
for item in items {
result.append(try transform(item))
}
return result
}
// No try needed if transform doesn't throw
let doubled = process([1, 2, 3]) { $0 * 2 }
Practical Example
func loadUser(from json: String) throws -> User {
guard let data = json.data(using: .utf8) else {
throw UserError.invalidData
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
return try decoder.decode(User.self, from: data)
} catch {
throw UserError.decodingFailed(underlying: error)
}
}
enum UserError: Error {
case invalidData
case decodingFailed(underlying: Error)
}
The do/try/catch pattern is Swift's primary error handling mechanism. It makes error paths explicit and forces you to handle errors at compile time.
Result Type
Result Type
Result<Success, Failure> is a generic enum that represents either a success or a failure. It provides an alternative to throwing errors, especially useful in asynchronous code and APIs where you want explicit success/failure handling.
Result Definition
enum Result<Success, Failure: Error> {
case success(Success)
case failure(Failure)
}
Creating Results
func divide(_ a: Int, by b: Int) -> Result<Int, MathError> {
guard b != 0 else {
return .failure(.divisionByZero)
}
return .success(a / b)
}
enum MathError: Error {
case divisionByZero
}
let result = divide(10, by: 2)
// .success(5)
Handling Results
switch result {
case .success(let value):
print("Result: \(value)")
case .failure(let error):
print("Error: \(error)")
}
Using map on Result
let doubled = divide(10, by: 2).map { $0 * 2 } // .success(20)
Converting Throws to Result
let result = Result { try divide(10, by: 0) } // .failure(.divisionByZero)
Result with Try
if case .success(let value) = result {
print(value)
}
// Or using value property (deprecated in newer Swift)
// let value = result.value
Practical Example: Network Layer
enum APIError: Error {
case invalidURL
case noData
case decodingFailed(Error)
}
func fetchUser(id: String, completion: @escaping (Result<User, APIError>) -> Void) {
guard let url = URL(string: "https://api.example.com/users/\(id)") else {
completion(.failure(.invalidURL))
return
}
URLSession.shared.dataTask(with: url) { data, _, error in
if let error = error {
completion(.failure(.decodingFailed(error)))
return
}
guard let data = data else {
completion(.failure(.noData))
return
}
do {
let user = try JSONDecoder().decode(User.self, from: data)
completion(.success(user))
} catch {
completion(.failure(.decodingFailed(error)))
}
}.resume()
}
Result vs Throwing Functions
| Feature | Throwing | Result |
|---|---|---|
| Syntax | try/catch |
Pattern matching |
| Async compatibility | Limited (pre-concurrency) | Excellent |
| Composability | do/try/catch nesting | map, flatMap chaining |
| Explicit error type | No (any Error) | Yes (typed) |
flatMap on Result
let result = divide(10, by: 2)
.flatMap { divide($0, by: 3) }
// .success(1) (5 / 3 = 1)
Result type provides typed, composable error handling that works well with closures and async patterns.
Quiz
1. What protocol must error types conform to?
2. What does `try?` do?
3. What does Result.failure contain?
4. When should you use `try!`?
5. What makes a function `rethrows`?
Flashcards
Question
How do you define a custom error in Swift?
Click to reveal answer
Answer
Create an enum conforming to `Error`, with cases for each error type: `enum MyError: Error { case foo }`
Question
What is the difference between `try?` and `try!`?
Click to reveal answer
Answer
`try?` returns Optional (nil on error); `try!` crashes on error.
Question
What does Result<Success, Failure> represent?
Click to reveal answer
Answer
Either .success(Success) or .failure(Failure), where Failure must conform to Error.
Question
How do you propagate errors without catching them?
Click to reveal answer
Answer
Mark the function with `throws` and the errors automatically propagate to the caller.
Question
What is LocalizedError used for?
Click to reveal answer
Answer
Providing user-friendly error descriptions via `errorDescription` computed property.
Revision Notes
Key Takeaways
- 1. Custom errors are typically enums conforming to Error
- 2. do/try/catch is Swift's primary error handling mechanism
- 3. try? converts errors to optionals; try! crashes on error
- 4. Result type provides typed success/failure handling
- 5. LocalizedError enables user-friendly error messages
Interview Tips
- • Explain when to use Result vs throwing functions
- • Know the difference between try?, try!, and regular try
- • Be able to design a custom error hierarchy
- • Understand rethrows and when to use it
Cheat Sheet
Error Handling Cheat Sheet
Defining Errors:
enum MyError: Error { case foo, bar }
Throwing:
func doWork() throws { throw MyError.foo }
Catching:
do { try doWork() } catch MyError.foo { ... } catch { ... }
Shorthand:
try?— nil on errortry!— crashes on error
Result Type:
Result<Int, Error> // .success(42) or .failure(error)
Rethrows:
func process(_ f: () throws -> Void) rethrows { try f() }