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
catchblock'serroris already typed asNetworkError, so theswitchis exhaustive without anas?cast or a fallbackcatchfor 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) -> Usercomposes 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.