Swift Error Propagation
Error handling is a crucial part of building robust applications. In Swift, the error propagation system allows errors to bubble up through your code, giving you flexibility in where and how you handle them. This concept enables you to separate error detection from error handling, making your code cleaner and more maintainable.
What is Error Propagation?
Error propagation is the mechanism that allows functions to pass errors they encounter up to the calling function. Instead of requiring every function to handle all possible errors internally, Swift lets you mark functions that can "throw" errors, allowing the calling code to decide how to handle them.
This creates a chain of responsibility where errors flow up through your call stack until they reach code that's equipped to handle them.
How to Mark a Function That Propagates Errors
To indicate that a function can throw errors, you add the throws
keyword to its declaration. This signals to the caller that they need to handle potential errors.
func readFile(named filename: String) throws -> String {
// Function implementation that might throw an error
if !fileExists(filename) {
throw FileError.notFound
}
return "File contents"
}
How to Call Functions That Can Throw Errors
When calling a function that's marked with throws
, you need to use the try
keyword along with one of three error handling mechanisms:
1. Using try
with do-catch
The most comprehensive way to handle potential errors is using a do-catch
block:
do {
let contents = try readFile(named: "myFile.txt")
print("File contents: \(contents)")
} catch FileError.notFound {
print("Error: File not found")
} catch {
print("Unexpected error: \(error)")
}
// Output if file doesn't exist:
// Error: File not found
2. Using try?
to Convert Errors to Optionals
For simpler error handling, you can use try?
to convert the result to an optional:
let contents = try? readFile(named: "myFile.txt")
// If an error is thrown, contents will be nil
// Otherwise, contents will contain the file contents
if let fileContents = contents {
print("File contents: \(fileContents)")
} else {
print("Couldn't read file")
}
// Output if file doesn't exist:
// Couldn't read file
3. Using try!
When You're Certain No Error Will Occur
In rare cases when you're absolutely certain an operation won't fail (use with caution):
// Only use when you're 100% sure the call won't fail
let contents = try! readFile(named: "definitelyExists.txt")
// This will crash if an error is thrown
Propagating Errors in Practice
Let's see how error propagation works with multiple functions:
// Define custom error types
enum FileError: Error {
case notFound
case noPermission
case corrupt
}
// A function that can throw errors
func readFile(named filename: String) throws -> String {
// Simulate file operations that might fail
if filename.hasSuffix(".corrupt") {
throw FileError.corrupt
} else if filename == "private.txt" {
throw FileError.noPermission
} else if filename == "missing.txt" {
throw FileError.notFound
}
return "Contents of \(filename)"
}
// A function that calls readFile and propagates errors upward
func processDocument(named filename: String) throws -> String {
// The 'throws' keyword allows this function to propagate errors
let contents = try readFile(named: filename)
return "Processed: \(contents)"
}
// A function that handles errors
func displayDocument(named filename: String) {
do {
let processed = try processDocument(named: filename)
print(processed)
} catch FileError.notFound {
print("Error: The file \(filename) does not exist.")
} catch FileError.noPermission {
print("Error: You don't have permission to read \(filename).")
} catch FileError.corrupt {
print("Error: The file \(filename) is corrupted.")
} catch {
print("An unexpected error occurred: \(error)")
}
}
// Call the function with different filenames
displayDocument(named: "normal.txt")
displayDocument(named: "missing.txt")
displayDocument(named: "private.txt")
displayDocument(named: "data.corrupt")
// Output:
// Processed: Contents of normal.txt
// Error: The file missing.txt does not exist.
// Error: You don't have permission to read private.txt.
// Error: The file data.corrupt is corrupted.
In this example, notice how:
readFile
can throw errorsprocessDocument
callsreadFile
withtry
and propagates any errors upward withthrows
displayDocument
is the final handler that actually deals with the errors
Real-world Application: Network Request Handler
Here's a practical example of error propagation in a network request scenario:
enum NetworkError: Error {
case invalidURL
case noConnection
case serverError(code: Int)
case invalidData
}
// Function that performs a network request and can throw errors
func fetchData(from urlString: String) throws -> Data {
guard let url = URL(string: urlString) else {
throw NetworkError.invalidURL
}
// Simplified example - in real code, you'd use URLSession
if urlString.contains("invalid") {
throw NetworkError.invalidURL
} else if urlString.contains("offline") {
throw NetworkError.noConnection
} else if urlString.contains("error") {
throw NetworkError.serverError(code: 500)
}
// Return some dummy data if no errors
return "Response data".data(using: .utf8)!
}
// Function that processes the data and propagates errors
func processUserProfile(from urlString: String) throws -> [String: String] {
let data = try fetchData(from: urlString)
// Simulate data processing that might fail
if data.isEmpty {
throw NetworkError.invalidData
}
// Return processed data
return ["name": "John Doe", "email": "[email protected]"]
}
// Function that displays user profile and handles errors
func displayUserProfile(from urlString: String) {
do {
let profile = try processUserProfile(from: urlString)
print("User profile: \(profile)")
} catch NetworkError.invalidURL {
print("Error: Invalid URL provided")
} catch NetworkError.noConnection {
print("Error: No internet connection")
} catch NetworkError.serverError(let code) {
print("Error: Server returned error code \(code)")
} catch NetworkError.invalidData {
print("Error: Could not process the data received")
} catch {
print("Unexpected error: \(error)")
}
}
// Test with different scenarios
displayUserProfile(from: "https://api.example.com/user/123")
displayUserProfile(from: "https://api.example.com/offline/user/123")
displayUserProfile(from: "https://api.example.com/error/user/123")
displayUserProfile(from: "invalid-url")
// Output:
// User profile: ["name": "John Doe", "email": "[email protected]"]
// Error: No internet connection
// Error: Server returned error code 500
// Error: Invalid URL provided
Advanced: Rethrowing Functions with rethrows
Swift also allows functions that take throwing function as a parameter to propagate errors only if the parameter function throws. This is done with the rethrows
keyword:
// A function that rethrows errors from its function parameter
func executeAndLog<T>(operation: () throws -> T) rethrows -> T {
print("Operation starting...")
let result = try operation() // Will rethrow if operation throws
print("Operation completed successfully")
return result
}
// Usage example
do {
// This won't throw an error
let result1 = try executeAndLog {
return "Success!"
}
print("Result: \(result1)")
// This will throw an error
let result2 = try executeAndLog {
throw FileError.notFound
}
print("Result: \(result2)") // This line never executes
} catch {
print("Caught error: \(error)")
}
// Output:
// Operation starting...
// Operation completed successfully
// Result: Success!
// Operation starting...
// Caught error: notFound
The rethrows
keyword is especially useful for higher-order functions like map
, filter
, and reduce
in Swift's standard library.
Summary
Error propagation in Swift provides a clean, type-safe way to separate error detection from error handling:
- Use
throws
to mark functions that can generate errors - Use
try
when calling functions that might throw errors - Choose between
do-catch
,try?
, ortry!
based on your error handling needs - Errors propagate up the call stack until they're handled
- Use
rethrows
for higher-order functions that might propagate errors from function parameters
This system gives you flexibility in deciding where in your code to handle errors while maintaining clarity and type safety.
Additional Resources
Exercise
-
Create a file management system that includes functions for creating, reading, updating, and deleting files. Implement appropriate error types and propagation chains.
-
Enhance the network request example above to include JSON parsing that can throw errors, and add proper error handling for different HTTP status codes.
-
Implement a function that takes an array of URLs as strings, attempts to fetch data from each one, and returns all successful results while reporting errors for failed requests.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)