← All shorts
63Swift 6+2 min read

Typed Throws and Richer Error Signatures

Typed throws let a function declare exactly which error type it throws, so callers can catch it exhaustively without casting.

A plain throws function only tells you it can throw something conforming to Error — the actual set of cases lives in documentation, not the signature, and a catch block has no way to switch over it exhaustively without downcasting first.

#The old way

enum NetworkError: Error {
    case unauthorized
    case notFound
    case serverError(code: Int)
}

func fetchUser(id: String) throws -> User {
    guard !id.isEmpty else {
        throw NetworkError.notFound
    }
    return User(id: id, name: "Loaded")
}

do {
    let user = try fetchUser(id: "")
    print(user)
} catch let error as NetworkError {
    switch error {
    case .unauthorized: print("Log in again")
    case .notFound: print("No such user")
    case .serverError(let code): print("Server error \(code)")
    }
} catch {
    print("Unexpected error: \(error)")
}

#The new way

struct User { let id: String; let name: String }

enum NetworkError: Error {
    case unauthorized
    case notFound
    case serverError(code: Int)
}

func fetchUser(id: String) throws(NetworkError) -> User {
    guard !id.isEmpty else {
        throw NetworkError.notFound
    }
    return User(id: id, name: "Loaded")
}

do {
    let user = try fetchUser(id: "")
    print(user)
} catch {
    switch error {
    case .unauthorized: print("Log in again")
    case .notFound: print("No such user")
    case .serverError(let code): print("Server error \(code)")
    }
}

#Why it matters

  • The catch block's error is already typed as NetworkError, so the switch is exhaustive without an as? cast or a fallback catch for the untyped case
  • The function signature documents its failure modes directly — callers and code review both see the exact error type without opening the implementation
  • throws(Never) is a legal, useful signature for a function that's syntactically throwing (to satisfy a protocol) but never actually does
  • Works the same with async: func fetchUser(id: String) async throws(NetworkError) -> User composes normally

#Gotcha

Typed throws don't union automatically — a function that calls two different throwing functions with two different typed errors can't declare a single narrow throws(SomeError) unless it catches and remaps both into that one type itself; otherwise it has to fall back to plain throws (equivalent to throws(any Error)). This makes typed throws a good fit for leaf functions with one well-defined error enum, but composing several typed-throwing layers usually still ends in an error-mapping catch somewhere rather than one error type propagating cleanly to the top.

swiftconcurrency

Related shorts