The best examples of error handling in Swift: 3 practical examples you’ll actually use
Let’s start with the most common real example of error handling in Swift: network calls that fail in unpredictable ways.
You might be fetching data from an API, and any of these can happen:
- The user is offline.
- The server returns a non-200 status code.
- The JSON payload shape changed.
Here’s a practical NetworkError enum and a tiny client using async/await:
enum NetworkError: Error {
case invalidURL
case transportError(underlying: Error)
case serverError(statusCode: Int)
case decodingError(underlying: Error)
}
struct User: Decodable {
let id: Int
let name: String
}
final class APIClient {
private let baseURL = URL(string: "https://api.example.com")
func fetchUser(id: Int) async throws -> User {
guard let baseURL else {
throw NetworkError.invalidURL
}
let url = baseURL.appendingPathComponent("/users/\(id)")
do {
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.transportError(underlying: URLError(.badServerResponse))
}
guard (200..<300).contains(httpResponse.statusCode) else {
throw NetworkError.serverError(statusCode: httpResponse.statusCode)
}
do {
return try JSONDecoder().decode(User.self, from: data)
} catch {
throw NetworkError.decodingError(underlying: error)
}
} catch {
throw NetworkError.transportError(underlying: error)
}
}
}
``
This is one of the best examples of error handling in Swift because it shows how to:
- Wrap underlying errors (`URLError`, decoding errors) in your own domain-specific enum.
- Distinguish between **transport** problems and **server** problems.
- Keep the throwing surface area small: callers just deal with `NetworkError`.
A caller might use it like this:
```swift
func loadUserProfile() async {
let client = APIClient()
do {
let user = try await client.fetchUser(id: 42)
print("Loaded user: \(user.name)")
} catch let error as NetworkError {
switch error {
case .invalidURL:
showError("App configuration error. Please contact support.")
case .transportError:
showError("You appear to be offline. Please check your connection.")
case .serverError(let statusCode):
showError("Server error (\(statusCode)). Please try again later.")
case .decodingError:
showError("We’re having trouble reading data from the server.")
}
} catch {
showError("Unexpected error: \(error.localizedDescription)")
}
}
This first case is a clean example of mapping low-level failures into user-friendly messages. If you’re looking for examples of error handling in Swift: 3 practical examples that mirror real apps, this network scenario should be at the top of your list.
2. JSON parsing and data validation: more real examples of error handling in Swift
The second of our three practical examples focuses on decoding and validation. Swift’s Codable makes decoding data easy, but real APIs are messy. Fields are missing, types don’t match, and sometimes the data is just wrong.
Here’s a realistic model for a product in an e-commerce app, with validation layered on top of decoding:
enum ProductError: Error {
case missingField(String)
case invalidPrice
case invalidStock
}
struct Product: Decodable {
let id: Int
let name: String
let price: Decimal
let stock: Int
enum CodingKeys: String, CodingKey {
case id, name, price, stock
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
guard let id = try? container.decode(Int.self, forKey: .id) else {
throw ProductError.missingField("id")
}
guard let name = try? container.decode(String.self, forKey: .name) else {
throw ProductError.missingField("name")
}
guard let price = try? container.decode(Decimal.self, forKey: .price) else {
throw ProductError.missingField("price")
}
guard let stock = try? container.decode(Int.self, forKey: .stock) else {
throw ProductError.missingField("stock")
}
guard price >= 0 else { throw ProductError.invalidPrice }
guard stock >= 0 else { throw ProductError.invalidStock }
self.id = id
self.name = name
self.price = price
self.stock = stock
}
}
This gives you fine-grained control: you know whether the data was missing, or present but invalid. That’s a pattern worth copying when you look for the best examples of error handling in Swift.
Now imagine you’re loading a list of products and want partial success: decode what you can, log the rest.
func decodeProducts(from data: Data) -> [Product] {
let decoder = JSONDecoder()
do {
return try decoder.decode([Product].self, from: data)
} catch let error as ProductError {
// You might send this to your logging backend
print("Product decoding error: \(error)")
return []
} catch {
print("Unexpected decoding error: \(error)")
return []
}
}
If you want even more nuanced behavior, you can use Result to capture both success and failure values for each element:
func decodeProductsIndividually(from data: Data) -> [Result<Product, Error>] {
struct Wrapper: Decodable { let products: [Product] }
let decoder = JSONDecoder()
do {
let wrapper = try decoder.decode(Wrapper.self, from: data)
return wrapper.products.map { .success($0) }
} catch {
// Fallback: try to decode as an array of loosely-typed dictionaries
guard let jsonArray = try? JSONSerialization.jsonObject(with: data) as? [Any] else {
return [.failure(error)]
}
return jsonArray.map { element in
do {
let productData = try JSONSerialization.data(withJSONObject: element)
let product = try decoder.decode(Product.self, from: productData)
return .success(product)
} catch {
return .failure(error)
}
}
}
}
This gives you a real example of partial failure handling: some items succeed, some fail, and you keep going.
3. User input validation: examples of error handling in Swift: 3 practical examples in UI flows
The third of our examples of error handling in Swift: 3 practical examples focuses on user input. This is where you’ll spend a lot of time in 2024–2025, especially if you’re working with sign-up forms, payment details, or any regulated data.
Let’s say you’re validating a sign-up form:
enum ValidationError: LocalizedError {
case emptyEmail
case invalidEmailFormat
case weakPassword
case passwordTooShort(minLength: Int)
var errorDescription: String? {
switch self {
case .emptyEmail:
return "Email can’t be empty."
case .invalidEmailFormat:
return "Please enter a valid email address."
case .weakPassword:
return "Password must include letters and numbers."
case .passwordTooShort(let minLength):
return "Password must be at least \(minLength) characters."
}
}
}
struct SignUpForm {
let email: String
let password: String
}
func validate(_ form: SignUpForm) throws {
guard !form.email.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
throw ValidationError.emptyEmail
}
// Very simple email check: you’d use something stronger in production
guard form.email.contains("@"), form.email.contains(".") else {
throw ValidationError.invalidEmailFormat
}
let minLength = 8
guard form.password.count >= minLength else {
throw ValidationError.passwordTooShort(minLength: minLength)
}
let hasLetter = form.password.range(of: "[A-Za-z]", options: .regularExpression) != nil
let hasDigit = form.password.range(of: "[0-9]", options: .regularExpression) != nil
guard hasLetter && hasDigit else {
throw ValidationError.weakPassword
}
}
A SwiftUI view can catch and display these errors cleanly:
@MainActor
final class SignUpViewModel: ObservableObject {
@Published var email = ""
@Published var password = ""
@Published var errorMessage: String?
func submit() {
let form = SignUpForm(email: email, password: password)
do {
try validate(form)
// Proceed with API call
errorMessage = nil
} catch let error as LocalizedError {
errorMessage = error.errorDescription
} catch {
errorMessage = "Something went wrong. Please try again."
}
}
}
This is one of the best examples of error handling in Swift for UI work: it keeps validation logic testable and separate from the view, while still surfacing friendly messages.
If you’re handling sensitive data (health, finance), take a look at guidelines from organizations like Harvard’s cybersecurity resources for broader security practices around input and data handling.
4. File I/O and persistence: more examples include local storage failures
Beyond our main examples of error handling in Swift: 3 practical examples, real apps also read and write files: caching images, storing small JSON blobs, or exporting reports.
Here’s a small file storage helper that shows another example of error handling in Swift using a custom error type:
enum FileStorageError: Error {
case directoryNotFound
case writeFailed(underlying: Error)
case readFailed(underlying: Error)
case fileNotFound
}
final class FileStorage {
private let fileManager = FileManager.default
private func documentsDirectory() throws -> URL {
guard let url = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
throw FileStorageError.directoryNotFound
}
return url
}
func save(_ data: Data, as fileName: String) throws {
let directory = try documentsDirectory()
let fileURL = directory.appendingPathComponent(fileName)
do {
try data.write(to: fileURL, options: .atomic)
} catch {
throw FileStorageError.writeFailed(underlying: error)
}
}
func load(fileName: String) throws -> Data {
let directory = try documentsDirectory()
let fileURL = directory.appendingPathComponent(fileName)
guard fileManager.fileExists(atPath: fileURL.path) else {
throw FileStorageError.fileNotFound
}
do {
return try Data(contentsOf: fileURL)
} catch {
throw FileStorageError.readFailed(underlying: error)
}
}
}
This gives you another practical example of how to:
- Wrap system errors in a domain-specific enum.
- Differentiate between “file not found” (maybe fine) and real read/write failures.
You might intentionally treat fileNotFound as a non-fatal case and just return default data.
5. Async streams and cancellation: modern examples of error handling in Swift (2024–2025)
Swift’s concurrency model keeps evolving, and in 2024–2025 many teams are migrating from callback-based APIs to async sequences. That opens up newer patterns for error handling.
Here’s a concrete example of error handling in Swift using AsyncThrowingStream to wrap a WebSocket-like API that can fail mid-stream:
enum StreamError: Error {
case disconnected
case protocolViolation
}
func messageStream() -> AsyncThrowingStream<String, Error> {
AsyncThrowingStream { continuation in
let connection = WebSocketConnection(url: URL(string: "wss://example.com/socket")!)
connection.onMessage = { message in
continuation.yield(message)
}
connection.onError = { error in
continuation.finish(throwing: error)
}
connection.onClose = { code in
if code == .protocolError {
continuation.finish(throwing: StreamError.protocolViolation)
} else {
continuation.finish(throwing: StreamError.disconnected)
}
}
continuation.onTermination = { @Sendable _ in
connection.close()
}
connection.connect()
}
}
And consuming it:
func listenToMessages() async {
do {
for try await message in messageStream() {
print("Received: \(message)")
}
} catch {
print("Stream ended with error: \(error)")
}
}
This pattern is increasingly common in 2024–2025, especially for chat, live updates, and telemetry. It’s a modern example of how error handling in Swift plays nicely with async sequences and cancellation.
6. When not to throw: examples include optionals and Result
Not every failure deserves a thrown error. Some of the best examples of error handling in Swift actually show you when not to use throw.
Optional for simple, expected failures
For simple conversions, optionals are often enough:
func int(from string: String) -> Int? {
Int(string)
}
No need for an error type here; the caller can decide what to do with nil.
Result for boundary layers
At boundaries—like bridging to Objective-C, or exposing an API to other modules—Result can be a good fit:
func fetchConfiguration(completion: @escaping (Result<[String: Any], Error>) -> Void) {
Task {
do {
let data = try await someAsyncConfigFetch()
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] ?? [:]
completion(.success(json))
} catch {
completion(.failure(error))
}
}
}
This wraps async/await into a callback-based API, which is still common when you integrate with older codebases.
7. Design tips drawn from these examples of error handling in Swift
Looking across these examples of error handling in Swift: 3 practical examples (plus the extra scenarios), a few patterns stand out:
- Use domain-specific error enums (
NetworkError,ValidationError) rather than throwing raw system errors. - Implement
LocalizedErrorwhen you need user-facing messages. - Wrap underlying errors so you keep debug detail without leaking it into the UI.
- Reserve throwing for exceptional or boundary-crossing failures; use optionals for common, expected misses.
- In async code, treat cancellation separately when it matters for your UX.
If you’re working in regulated domains like health or finance, also consider broader guidance on reliability and data integrity. While Apple’s developer documentation is your primary technical source, general reliability and error-reporting practices are also discussed in engineering contexts at universities such as MIT OpenCourseWare and in broader software engineering curricula at Stanford Engineering.
FAQ: short answers and more examples of error handling in Swift
What are some real examples of error handling in Swift in production apps?
Real apps often use error handling in Swift for:
- Network failures (offline, timeouts, 500 errors)
- JSON decoding issues when APIs change
- User input validation (sign-up, payment, profile forms)
- File I/O problems (missing files, write failures)
- Streaming connections (WebSocket disconnects)
The examples in this article mirror those scenarios so you can copy the patterns directly.
Can you show an example of simple error handling with do–try–catch?
Here’s a minimal example of do–try–catch around a throwing function:
enum MathError: Error { case divisionByZero }
func divide(_ a: Double, by b: Double) throws -> Double {
guard b != 0 else { throw MathError.divisionByZero }
return a / b
}
func safeDivide() {
do {
let result = try divide(10, by: 0)
print(result)
} catch MathError.divisionByZero {
print("Cannot divide by zero.")
} catch {
print("Unexpected error: \(error)")
}
}
How do I decide between throwing errors and returning optionals?
Use optionals when failure is common and not surprising (e.g., parsing an Int from a string). Use throwing errors when you need to communicate why something failed, or when the failure crosses a boundary (network, disk, database) and you care about logging, analytics, or user feedback.
Are these examples of error handling in Swift compatible with Swift 5.9 and iOS 17?
Yes. All the examples use language features that are stable in Swift 5.9 and supported on current Apple platforms, including async/await and AsyncThrowingStream. For older OS versions, you may need to wrap async code with callbacks, but the error-design patterns stay the same.
Related Topics
Practical examples of simple calculator app examples in Swift
Practical examples of Swift async/await examples for asynchronous programming
Best examples of practical examples of using closures in Swift
Practical examples of UITableView implementation in Swift for modern iOS apps
Practical examples of timer examples with Grand Central Dispatch in Swift
The best examples of error handling in Swift: 3 practical examples you’ll actually use
Explore More Swift Code Snippets
Discover more examples and insights in this category.
View All Swift Code Snippets