BS
BleepingSwift
Published on
5 min read
Intermediate

> Typed Throws in Swift 6: Declaring Specific Error Types

Share:

Before Swift 6, the throws keyword was all-or-nothing. A throwing function could throw any error conforming to Error, and callers had to handle the general case. Swift 6 changes this with typed throws, letting you specify exactly which error type a function throws.

The Old Way

In Swift 5 and earlier, you'd write:

Swift
enum NetworkError: Error {
    case noConnection
    case timeout
    case invalidResponse
}

func fetchData() throws -> Data {
    throw NetworkError.noConnection
}

do {
    let data = try fetchData()
} catch let error as NetworkError {
    // Handle NetworkError
} catch {
    // Handle any other error
}

The compiler doesn't know that fetchData only throws NetworkError. You need the catch-all clause even if you've handled every case.

Typed Throws Syntax

Swift 6 lets you declare the error type:

Swift
enum NetworkError: Error {
    case noConnection
    case timeout
    case invalidResponse
}

func fetchData() throws(NetworkError) -> Data {
    throw NetworkError.noConnection
}

Now the compiler knows exactly what can be thrown. Callers get exhaustive checking:

Swift
do {
    let data = try fetchData()
} catch .noConnection {
    print("No network connection")
} catch .timeout {
    print("Request timed out")
} catch .invalidResponse {
    print("Invalid server response")
}

No catch-all needed. If you add a new case to NetworkError, the compiler will flag every call site that doesn't handle it.

Combining with Result

Typed throws pairs naturally with Result. You can convert between them cleanly:

Swift
enum ParseError: Error {
    case invalidFormat
    case missingField(String)
}

func parseJSON(_ data: Data) throws(ParseError) -> User {
    guard let json = try? JSONSerialization.jsonObject(with: data) else {
        throw .invalidFormat
    }
    guard let dict = json as? [String: Any], let name = dict["name"] as? String else {
        throw .missingField("name")
    }
    return User(name: name)
}

// Convert to Result
func parseJSONResult(_ data: Data) -> Result<User, ParseError> {
    Result { try parseJSON(data) }
}

The Result type automatically picks up ParseError as its Failure type.

Throwing Never

A function that can't fail can declare throws(Never):

Swift
func alwaysSucceeds() throws(Never) -> Int {
    return 42
}

// Can call without try
let value = alwaysSucceeds()

This is equivalent to a non-throwing function but useful when you need to satisfy a protocol requirement:

Swift
protocol DataProvider {
    associatedtype Failure: Error
    func fetch() throws(Failure) -> Data
}

struct InMemoryProvider: DataProvider {
    typealias Failure = Never

    let data: Data

    func fetch() throws(Never) -> Data {
        return data
    }
}

Generic Error Types

You can write generic code that preserves error types:

Swift
func transform<T, E: Error>(_ value: T, using closure: (T) throws(E) -> T) throws(E) -> T {
    try closure(value)
}

enum MathError: Error {
    case divisionByZero
}

func safeDivide(_ a: Int, by b: Int) throws(MathError) -> Int {
    guard b != 0 else { throw .divisionByZero }
    return a / b
}

let result = try transform(100) { value in
    try safeDivide(value, by: 5)
}

The error type flows through, so transform throws MathError specifically.

Async Functions

Typed throws works with async functions:

Swift
enum DownloadError: Error {
    case networkUnavailable
    case invalidURL
    case serverError(Int)
}

func download(from urlString: String) async throws(DownloadError) -> Data {
    guard let url = URL(string: urlString) else {
        throw .invalidURL
    }

    do {
        let (data, response) = try await URLSession.shared.data(from: url)
        guard let httpResponse = response as? HTTPURLResponse else {
            throw .serverError(0)
        }
        guard httpResponse.statusCode == 200 else {
            throw .serverError(httpResponse.statusCode)
        }
        return data
    } catch is URLError {
        throw .networkUnavailable
    } catch {
        throw .serverError(0)
    }
}

Rethrowing with Typed Errors

The rethrows keyword also works with typed throws:

Swift
func withRetry<T, E: Error>(
    attempts: Int,
    operation: () throws(E) -> T
) throws(E) -> T {
    var lastError: E?

    for _ in 0..<attempts {
        do {
            return try operation()
        } catch {
            lastError = error
        }
    }

    throw lastError!
}

Migration Considerations

Existing throws functions are equivalent to throws(any Error). You can call a typed-throws function from a regular throwing context:

Swift
enum MyError: Error {
    case failed
}

func typedFunction() throws(MyError) -> Int {
    throw .failed
}

func legacyFunction() throws -> Int {
    // This works - MyError is erased to any Error
    try typedFunction()
}

Going the other direction requires handling the general case:

Swift
func callLegacy() throws(MyError) -> Int {
    do {
        return try legacyFunction()
    } catch let error as MyError {
        throw error
    } catch {
        // Must handle unknown errors somehow
        fatalError("Unexpected error: \(error)")
    }
}

When to Use Typed Throws

Typed throws is most valuable for library APIs where you want to communicate exactly what can go wrong. Internal code might not need the extra precision.

Good candidates include network layers with well-defined failure modes, parsers with specific error cases, and any API where exhaustive error handling matters.

The tradeoff is flexibility. If you later need to add error cases, callers must update their catch blocks. Design your error types carefully to avoid breaking changes.

subscribe.sh

// Stay Updated

Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.

>

By subscribing, you agree to our Privacy Policy.