---
title: "Swift Error Handling: throws, Result, and Typed Errors"
slug: "swift-error-handling"
category: "coding"
tags: ["swift", "error-handling", "throws", "result", "swift6"]
status: "stable"
last_updated: 2026-05-14
summary: "Use throws over optionals, group errors by domain, leverage typed throws in Swift 6, and handle failures at the boundary."
related: ["[[coding/swift]]", "[[coding/swift-async-await]]", "[[coding/swift-optionals]]", "[[coding/swift-protocols]]", "[[ios/core-data]]"]
---

## Overview

Swift's error model is explicit: a function that can fail must declare it with `throws`, and callers must acknowledge failures with `try`. This page covers when to use `throws` versus optionals or `Result`, how to group errors by domain, typed throws in Swift 6, and the boundary rule. Read [[coding/swift]] for the overview, and [[coding/swift-optionals]] for the distinction between absence and failure.

## Prefer `throws` over optionals to signal failure

An optional return communicates absence; a thrown error communicates failure with a reason. Use `throws` when the caller needs to know why an operation failed.

```swift
// Bad: nil tells the caller nothing
func parseDate(_ string: String) -> Date? { ... }

// Better: the caller knows what went wrong
enum DateParseError: Error {
    case emptyInput
    case invalidFormat(String)
    case outOfRange(Date)
}

func parseDate(_ string: String) throws -> Date { ... }
```

Use `try?` only when failure genuinely means "skip and continue" and the reason is irrelevant to the caller. Use `try!` only in tests or in application-startup code where failure is a programmer error that should crash loudly.

## Group errors by domain, not by operation

One error type per subsystem keeps switch statements exhaustive and short. An error type with one case per function call creates massive switch exhaustion across callers.

```swift
// Too granular: one type per operation
enum FetchProfileError: Error { case networkFailure, decodingFailure }
enum SaveProfileError: Error { case diskFull, permissionDenied }

// Better: one type for the whole persistence domain
enum PersistenceError: Error {
    case networkFailure(URLError)
    case decodingFailure(DecodingError)
    case diskFull
    case permissionDenied
}
```

Name the type after the domain it belongs to. Internal code lets errors propagate; the entry point for each subsystem catches and translates into the next domain's error type. See [[coding/swift-async-await]] for how this interacts with async boundaries.

## Use typed throws in Swift 6 for well-bounded error domains

Swift 6 allows `throws(ErrorType)` to specify the exact error type a function throws. The compiler enforces exhaustive handling without an `as?` cast.

```swift
enum CacheError: Error {
    case miss(key: String)
    case expired(key: String, at: Date)
    case corrupted(key: String)
}

func load(key: String) throws(CacheError) -> Data { ... }

// Caller's do-catch is exhaustive without needing a default clause
do {
    let data = try load(key: "profile")
    process(data)
} catch .miss(let key) {
    await fetchRemote(key: key)
} catch .expired(let key, _) {
    await refresh(key: key)
} catch .corrupted(let key) {
    await invalidate(key: key)
}
```

Use untyped `throws` (erased to `any Error`) when the function delegates to several subsystems that each have their own error types. Use typed throws when the caller must handle every case and the error set is closed.

## Use `Result` at asynchronous boundaries

`Result<Value, Failure>` is useful when you need to store a completed or failed operation and resolve it later, or when bridging a callback API that cannot be converted with `withCheckedContinuation`.

```swift
typealias FetchResult = Result<Data, URLError>

func cache(result: FetchResult, for key: String) {
    responseCache[key] = result
}

func resolve(key: String) throws -> Data {
    guard let result = responseCache[key] else { throw CacheError.miss(key: key) }
    return try result.get()
}
```

Inside fully async code, `async throws` is cleaner than returning `Result`. Reserve `Result` for the case where the outcome must be a value rather than a thrown error.

## Mark rethrowing functions with `rethrows`

A function that accepts a throwing closure and propagates its errors should be `rethrows`, not `throws`. This preserves the call site's non-throwing status when a non-throwing closure is passed.

```swift
func measure<T>(_ label: String, block: () throws -> T) rethrows -> T {
    let start = Date()
    defer { log(label, duration: Date().timeIntervalSince(start)) }
    return try block()
}

// Non-throwing call: no try needed at the call site
let result = measure("sort") { array.sorted() }

// Throwing call: compiler requires try
let decoded = try measure("decode") { try JSONDecoder().decode(Item.self, from: data) }
```

`rethrows` makes the function's throwing behavior conditional on its closure argument. Omitting it forces every caller to write `try` even when they pass a non-throwing closure.

## Handle failures at the boundary; propagate inside

Internal functions should let errors propagate with `throws`. The function that represents a user action or network event is the right place to catch, log, and translate.

```swift
// Internal: propagates freely
func fetchUser(id: UUID) async throws -> User {
    let data = try await session.data(from: endpoint(for: id)).0
    return try JSONDecoder().decode(User.self, from: data)
}

// Boundary: catches and maps to UI state
@MainActor
func loadProfile(id: UUID) async {
    do {
        profile = try await fetchUser(id: id)
    } catch is URLError {
        errorMessage = "Check your connection."
    } catch {
        errorMessage = "Something went wrong."
        logger.error("\(error)")
    }
}
```

This keeps the internal code clean and keeps error-handling policy in one place per subsystem. See [[coding/swift-actors]] for the `@MainActor` boundary pattern.

## Related

- [[coding/swift]]
- [[coding/swift-async-await]]
- [[coding/swift-optionals]]
- [[coding/swift-protocols]]
- [[coding/swift-actors]]
- [[ios/swiftui]]
- [[ios/core-data]]
