The best examples of error handling in Swift: 3 practical examples you’ll actually use

If you’re building real apps, you don’t want toy code—you want real examples of error handling in Swift that match what you hit in production. That’s where this guide comes in. We’ll walk through **examples of error handling in Swift: 3 practical examples** that mirror common problems: parsing bad JSON, failing network calls, and validating user input. Along the way, we’ll layer in more scenarios so you see how the same patterns scale. Instead of abstract theory, you’ll see concrete code that compiles in Swift 5.9+ and works with async/await. These examples of error handling in Swift include throwing functions, `do–try–catch`, custom error enums, and the newer `Result` and `AsyncThrowingStream` patterns. By the end, you’ll know how to design error types, decide when to throw versus return optionals, and how to surface meaningful messages to users without turning your codebase into a tangle of `if let` and `guard` statements.
Written by
Jamie
Published

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 LocalizedError when 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.

Explore More Swift Code Snippets

Discover more examples and insights in this category.

View All Swift Code Snippets