Handling Errors
1. Defining Error Types
| Error Type | Description | Usage |
|---|---|---|
| Error | Protocol defining the basic structure of an error | Implement to create custom errors |
| NSError | Foundation's error type, bridged to Swift's Error | Used for interoperability with Objective-C APIs |
| Swift.Error | Swift's native error protocol, adopted by all error types | Primary way to handle errors in Swift code |
Example: Custom Error Type
enum NetworkError: Error {
case noConnection
case timeout
case invalidURL
}
Example: Error with Associated Values
enum FileError: Error {
case fileNotFound(String)
case permissionDenied(String)
}
2. Throwing Errors
| Method | Description | Example |
|---|---|---|
| throw | Used to throw an error from a function or method | |
| rethrows | Indicates a function that can throw only if the passed closure throws | |
Example: Throwing Errors in a Function
enum NetworkError: Error {
case noConnection
case timeout
}
func fetchData() throws -> String {
// Simulate network call that might fail
if Bool.random() {
throw NetworkError.noConnection
}
return "Data received"
}
Note: The
throws keyword in a function signature indicates that the function can throw an error, and any caller must handle it using try, try?, or try!.
3. Using Throwing Functions
| Function | Description | Throws |
|---|---|---|
| func readFile(_ path: String) throws -> String | Reads content from a file at the given path | FileError |
| func saveFile(_ data: Data, to path: String) throws | Saves data to a file at the specified path | FileError |
| func fetchJSON(from url: URL) throws -> [String: Any] | Fetches JSON data from a URL | NetworkError |
Example: Using Throwing Functions
func readAndProcessFile(path: String) throws {
let content = try readFile(path)
let processed = process(content)
try saveFile(processed, to: "output.txt")
}
do {
try readAndProcessFile(path: "input.txt")
} catch FileError.missing {
print("File not found")
} catch NetworkError.timeout {
print("Request timed out")
}
4. Catching Errors
| Error Handling Method | Description | Usage |
|---|---|---|
| do-catch | Primary Swift error handling construct. Used to handle errors thrown by functions. | |
| catch with pattern matching | Allows catching specific error types or conditions using pattern matching. | |
| catch with multiple patterns | Handle multiple error types in a single catch block. | |
Example: Throwing and Catching Errors
enum NetworkError: Error {
case noConnection
case timeout
case invalidURL
}
func fetchData() throws -> String {
// Simulate network error
throw NetworkError.noConnection
}
do {
let data = try fetchData()
print(data)
} catch NetworkError.noConnection {
print("No internet connection")
} catch NetworkError.timeout {
print("Request timed out")
} catch {
print("An unknown error occurred: \(error)")
}
Note: The
try keyword is required before any function that can throw an error.
Warning: If you do not handle an error with
do-catch, your app will crash.
5. Converting Errors to Optionals
| Method | Description | Example |
|---|---|---|
| try? | Converts a throwing expression to an optional value | let file = try? String(contentsOfFile: "example.txt") |
| try! | Converts a throwing expression to a non-optional value | let file = try! String(contentsOfFile: "example.txt") |
Note:
try? returns nil if an error occurs, while try! will crash if an error is thrown.6. Disabling Error Propagation
| Approach | Description | Code Example |
|---|---|---|
| try! | Forces unwrapping of a throwing expression, crashes if error occurs | let file = try! FileHandle(forReadingFrom: url) |
| try? | Converts a throwing expression into an optional, returns nil if error occurs | let file = try? FileHandle(forReadingFrom: url) |
Note:
try! should be used sparingly as it can cause runtime crashes if not handled properly.7. Using Defer for Cleanup
The defer statement in Swift allows you to execute code just before the current scope exits. It's particularly useful for cleanup operations like closing files, releasing resources, or resetting state.
| Feature | Description | Example |
|---|---|---|
| defer | Ensures that a block of code is executed before the current scope exits. | |
Example: Resource Management with defer
func processFile() {
let file = openFile("input.txt")
defer {
closeFile(file)
}
let data = readFile(file)
process(data)
}
Note:
defer statements are executed in LIFO (Last In, First Out) order. The last defer block is executed first when exiting the scope.Key Points
deferensures cleanup code runs even if an error occurs.- Multiple
deferstatements are executed in reverse order of declaration. - Use
deferfor managing resources like file handles, network connections, or locks.
8. Creating Custom Error Types
| Property | Description | Example |
|---|---|---|
| Error Protocol | Swift's Error protocol is the base for all error types | struct MyError: Error { } |
| Error Cases | Custom errors can have multiple cases with associated values | enum NetworkError: Error { case timeout; case invalidURL(URL) } |
| Localized Error | Implement localizedDescription for user-friendly messages |
extension MyError: LocalizedError { var errorDescription: String? { "Something went wrong" } } |
Example: Custom Error with Associated Values
enum FileError: Error {
case fileNotFound(String)
case permissionDenied(String)
case corruptedFile(String)
}
extension FileError: LocalizedError {
var errorDescription: String? {
switch self {
case .fileNotFound(let fileName):
return "File not found: \(fileName)"
case .permissionDenied(let fileName):
return "Permission denied for file: \(fileName)"
case .corruptedFile(let fileName):
return "File corrupted: \(fileName)"
}
}
}
Example: Throwing and Catching Custom Errors
func readFile(_ fileName: String) throws -> String {
if fileName.isEmpty {
throw FileError.fileNotFound(fileName)
}
// Simulate file reading logic
return "File content"
}
do {
let content = try readFile("example.txt")
print(content)
} catch FileError.fileNotFound(let fileName) {
print("Caught file not found error: \(fileName)")
} catch {
print("An error occurred: \(error.localizedDescription)")
}
9. Rethrowing Functions
| Function | Description | Notes |
|---|---|---|
| func rethrowingFunction() throws -> String | Declares a function that can rethrow errors from a nested call | Must be marked with throws in signature |
| func handleAndRethrow() throws -> String | Handles error internally and rethrows it using throw |
Used to transform or log errors before rethrowing |
Example: Rethrowing a Thrown Error
func fetchData() throws -> String {
guard let data = try? String(contentsOfFile: "file.txt") else {
throw NSError(domain: "FileError", code: 1, userInfo: nil)
}
return data
}
func processAndRethrow() throws -> String {
do {
let content = try fetchData()
return content
} catch {
print("Error caught and rethrown: \(error)")
throw error // Rethrowing the original error
}
}
Example: Using Rethrowing in Higher-Order Functions
func processItems(_ items: [String], transform: (String) throws -> String) throws -> [String] {
return try items.map { item in
try transform(item)
}
}
func safeTransform(_ input: String) throws -> String {
guard !input.isEmpty else {
throw NSError(domain: "InputError", code: 2, userInfo: nil)
}
return input.uppercased()
}
let items = ["hello", "world"]
let result = try processItems(items, transform: safeTransform)
10. Understanding Error Propagation Patterns
| Pattern | Description | Usage |
|---|---|---|
| Throw and Catch | Errors are thrown using throw and caught with do-catch blocks. |
Used for handling expected errors in a structured way. |
| Error Propagation | Errors can be propagated up the call stack using try without catch. |
When a function can't handle an error, it lets the caller deal with it. |
| Result Type | Uses Result<Success, Error> to wrap success or failure values. |
Alternative to throwing errors, useful for functional programming styles. |
| Optional Chaining with Error Handling | Errors can be handled using optional chaining and nil values. |
Used in cases where a value might not be present or an operation fails. |
Example: Throw and Catch
enum NetworkError: Error {
case noConnection
case timeout
}
func fetchData() throws -> String {
// Simulate a network call that might fail
throw NetworkError.noConnection
}
do {
let data = try fetchData()
print(data)
} catch NetworkError.noConnection {
print("No internet connection")
} catch {
print("An error occurred: \(error)")
}
Example: Result Type
enum DataFetchResult {
case success(String)
case failure(Error)
}
func fetchWithResult() -> DataFetchResult {
return .failure(NetworkError.timeout)
}
let result = fetchWithResult()
switch result {
case .success(let data):
print("Data: \(data)")
case .failure(let error):
print("Error: \(error)")
}