- Published on
- 5 min read Intermediate
> Typed Throws in Swift 6: Declaring Specific Error Types
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:
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:
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:
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:
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):
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:
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:
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:
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:
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:
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:
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.
// Continue_Learning
Error Handling in Swift: From Basics to Typed Throws
A practical guide to Swift error handling covering throw/try/catch fundamentals, custom error types, Result, async error patterns, and typed throws introduced in Swift 6.
The Difference Between Frame and Bounds in UIKit
Understand the key difference between a UIView's frame and bounds properties, and when to use each in your iOS apps.
TaskGroup and Structured Concurrency Patterns in Swift
Master Swift's TaskGroup for parallel async work with proper cancellation, error handling, and result collection. Learn common patterns for batch operations and concurrent pipelines.
// Stay Updated
Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.