Error Handling
Swift provides a robust error-handling system using throw
, try
, and catch
to manage recoverable errors, ensuring code safety and clarity. Errors represent exceptional conditions, such as invalid input or network failures.
Defining Errors
Errors conform to the Error
protocol, typically implemented as enums for specific cases.
Example: Basic Error Enum:
enum NetworkError: Error {
case noConnection
case invalidURL(String)
case serverError(statusCode: Int)
}
enum ValidationError: Error {
case emptyInput
case invalidFormat(String)
}
Example: Custom Error with Details:
struct FileError: Error, CustomStringConvertible {
let code: Int
let message: String
var description: String {
return "FileError(\(code): \(message))"
}
}
Throwing Errors
Functions that can throw errors are marked with throws
.
Example: Throwing Function:
func fetchData(from urlString: String) throws -> Data {
guard urlString.contains("http") else {
throw NetworkError.invalidURL(urlString)
}
guard !NetworkManager.isOffline else {
throw NetworkError.noConnection
}
// Simulate fetch
return Data()
}
Example: Throwing Initializer:
struct Document {
let content: String
init(content: String) throws {
guard !content.isEmpty else {
throw ValidationError.emptyInput
}
self.content = content
}
}
Handling Errors
Use try
to call throwing functions within a do-catch
block, or use try?
/try!
for alternative handling.
Do-Catch Blocks
Catch specific or general errors.
Example: Specific Error Handling:
do {
let data = try fetchData(from: "invalid")
print("Data fetched: \(data.count) bytes")
} catch NetworkError.invalidURL(let url) {
print("Invalid URL: \(url)")
} catch NetworkError.noConnection {
print("No network connection")
} catch {
print("Unexpected error: \(error)")
}
Example: Nested Error Handling:
func processFile(_ path: String) throws -> String {
do {
let doc = try Document(path: path)
return doc.content.uppercased()
} catch ValidationError.emptyInput {
throw FileError(code: 400, message: "Empty file")
}
}
do {
let result = try processFile("")
print(result)
} catch let fileError as FileError {
print(fileError.description)
} catch {
print(error)
}
Try?
Converts errors to nil
.
Example:
let optionalData = try? fetchData(from: "invalid")
print(optionalData ?? "No data") // "No data"
Try!
Forces execution, crashing on errors.
Example:
// let data = try! fetchData("invalid") // Crashes on error
Propagating Errors
Rethrow errors in throwing functions.
Example:
func executeRequest(_ url: String) throws -> Data {
return try fetchData(from: url)
}
Defer Statements
Execute cleanup code regardless of success or failure.
Example:
func processResource() throws {
let resource = acquireResource()
defer { releaseResource(resource) }
guard isValid(resource) else {
throw ResourceError.invalid
}
// Process resource
}
Localized Error Descriptions
Conform to LocalizedError
for user-friendly messages.
Example:
extension NetworkError: LocalizedError {
var errorDescription: String? {
switch self {
case .noConnection:
return NSLocalizedString("No internet connection.", comment: "")
case .invalidURL(let url):
return NSLocalizedString("Invalid URL: \(url)", comment: "")
case .serverError(let code):
return NSLocalizedString("Server error with code \(code)", comment: "")
}
}
}
do {
try fetchData(from: "invalid")
} catch {
print(error.localizedDescription) // "Invalid URL: invalid"
}
Error Recovery Strategies
Implement retry or fallback logic.
Example:
func fetchWithRetry(url: String, retries: Int = 3) throws -> Data {
var lastError: Error?
for attempt in 1...retries {
do {
return try fetchData(from: url)
} catch {
lastError = error
print("Attempt \(attempt) failed: \(error)")
if attempt == retries { throw error }
Thread.sleep(forTimeInterval: 1.0)
}
}
throw lastError ?? NetworkError.noConnection
}
Best Practices
- Specific Errors: Use enums for granular error types.
- Localized Errors: Provide user-friendly messages.
- Defer Cleanup: Ensure resources are released.
- Handle Appropriately: Catch errors at the right level.
- Avoid Try!: Use only in guaranteed-safe cases.
- Retry Logic: Implement for transient errors (e.g., network).
- Document Throws: Specify error types in documentation.
Troubleshooting
- Missing Catch: Ensure all errors are caught or propagated.
- Unexpected Error: Cast to specific types for debugging.
- Resource Leaks: Verify
defer
statements execute. - Retry Failures: Log errors for each attempt.
- Localization Issues: Test localized strings in multiple languages.
Example: Comprehensive Error Handling
enum DatabaseError: Error, LocalizedError {
case notFound(id: String)
case connectionFailed
case invalidQuery(String)
var errorDescription: String? {
switch self {
case .notFound(let id):
return "Record with ID \(id) not found."
case .connectionFailed:
return "Database connection failed."
case .invalidQuery(let query):
return "Invalid query: \(query)."
}
}
}
class Database {
private var isConnected = false
func connect() throws {
defer { print("Cleaning up connection attempt") }
guard canConnect() else {
throw DatabaseError.connectionFailed
}
isConnected = true
}
func query(_ sql: String) throws -> [String: Any] {
guard isConnected else {
throw DatabaseError.connectionFailed
}
guard isValidQuery(sql) else {
throw DatabaseError.invalidQuery(sql)
}
// Simulate query
return ["id": "123", "name": "Alice"]
}
}
func executeQuery(_ db: Database, sql: String) throws -> [String: Any] {
do {
try db.connect()
return try db.query(sql)
} catch DatabaseError.connectionFailed {
print("Retrying connection...")
try db.connect()
return try db.query(sql)
}
}
let db = Database()
do {
let result = try executeQuery(db, "SELECT * FROM users")
print(result)
} catch {
print(error.localizedDescription)
}