What Are Typed and Untyped Throws?
Untyped Throws (Traditional Swift)
- Functions throw errors conforming to
Error
protocol - Actual error type erased to
any Error
at compile time - Catch blocks must handle all possible errors or use generic catch
- Been the standard since Swift 2.0
Typed Throws (Swift 6)
- Functions explicitly declare which error types they throw
- Compile-time guarantees about error types
- Enables exhaustive error handling without default catch
- Improves API clarity and type safety
Technical Implementation Details
Syntax Comparison
Untyped Throws (Classic Approach):
func fetchUser(id: String) throws -> User {
// Can throw any Error type
}
Typed Throws (Swift 6):
func fetchUser(id: String) throws(NetworkError) -> User {
// Can only throw NetworkError types
}
Key Implementation Points
1. Error Type Declaration
- Place error type in parentheses after
throws
keyword - Error type must conform to
Error
protocol - Single error type per function signature
2. Compiler Enforcement
- Only specified error type can be thrown
- Attempting to throw different error types results in compilation error
- Type information preserved through call chain
3. Multiple Error Types
- Use enum with associated values for multiple error scenarios
- Alternatively, create error protocol hierarchies
- Swift 6 doesn't support union types like
throws(NetworkError | ValidationError)
Type Erasure: The Hidden Cost of Untyped Throws
Understanding Type Erasure in Error Handling
Untyped Throws - Runtime Type Erasure:
- When a function uses plain
throws
, Swift erases the concrete error type - Errors boxed into existential container
any Error
- Runtime type information stored alongside error value
- Dynamic dispatch required for error handling
Typed Throws - Zero Type Erasure:
- Concrete error type preserved at compile time
- No existential container needed
- Direct memory layout without indirection
- Static dispatch for error handling
Memory and Performance Impact
// Untyped throws - requires type erasure
func fetchDataUntyped() throws -> Data {
throw NetworkError.timeout // Boxed as 'any Error'
}
// Typed throws - no type erasure
func fetchDataTyped() throws(NetworkError) -> Data {
throw NetworkError.timeout // Direct NetworkError type
}
// Memory layout comparison:
// Untyped: [Existential Container] -> [Type Metadata] -> [Error Value]
// Typed: [Error Value] (direct)
Type Erasure Overhead Breakdown
1. Existential Container Cost
- Heap allocation for errors
2. Runtime Type Checking
- Dynamic casts in catch blocks
3. Optimization Barriers
- Compiler cannot inline error handling paths
- No constant propagation across error boundaries
Practical Example: Authentication Service
Defining Typed Errors
enum AuthError: Error {
case invalidCredentials
case sessionExpired
case networkFailure(Int)
case twoFactorRequired
}
struct AuthToken {
let token: String
let expiresAt: Date
}
Compact Service Implementation
class AuthService {
func login(email: String, password: String) throws(AuthError) -> AuthToken {
// Validate inputs
guard isValidEmail(email), !password.isEmpty else {
throw AuthError.invalidCredentials
}
// Simulate API call
let response = mockAPICall(email: email, password: password)
switch response.status {
case 200:
return AuthToken(token: response.token, expiresAt: .distantFuture)
case 401:
throw AuthError.invalidCredentials
case 403:
throw AuthError.twoFactorRequired
case 440:
throw AuthError.sessionExpired
default:
throw AuthError.networkFailure(response.status)
}
}
private func isValidEmail(_ email: String) -> Bool {
email.contains("@") && email.contains(".")
}
private func mockAPICall(email: String, password: String) -> (status: Int, token: String) {
// Simplified mock response
return (status: 200, token: "mock_token_12345")
}
}
Clean Error Handling
class LoginViewModel {
private let authService = AuthService()
func performLogin(email: String, password: String) {
do {
let token = try authService.login(email: email, password: password)
storeToken(token)
navigateToHome()
} catch .invalidCredentials {
showAlert("Invalid email or password")
} catch .sessionExpired {
showAlert("Session expired. Please login again")
} catch .twoFactorRequired {
navigateToTwoFactor()
} catch .networkFailure(let code) {
showAlert("Network error: \(code)")
}
// Exhaustive - no default catch needed!
}
}
Key Benefits and Use Cases
Benefits of Typed Throws
1. Compile-Time Safety
- Exhaustive error handling without default catch
- Prevents accidentally ignoring specific error cases
- Refactoring safety when error types change
2. Self-Documenting APIs
- Error types visible in function signature
- No need to dig through implementation
- Clear contract between caller and implementation
3. Performance Optimization
- No type erasure overhead
- Direct error type dispatch
- Smaller binary size due to reduced generic code
4. Better IDE Support
- Autocomplete for specific error cases
- Inline documentation for error types
- Quick navigation to error definitions
Ideal Use Cases
1. Domain-Specific Libraries
- Network clients with defined error states
- Database operations with specific failure modes
- File system operations with known error conditions
2. Internal Module Boundaries
- Service layer to presentation layer communication
- Repository pattern implementations
- Use case/interactor error propagation
3. Public SDK Development
- Clear error contracts for SDK consumers
- Version-stable error handling
- Reduced support burden through explicit errors
Best Practices
1. Error Granularity
- Balance between too many and too few error cases
- Group related errors logically
- Consider client needs over implementation details
2. Error Evolution
- Use enums for closed error sets
- Add cases carefully to avoid breaking changes
3. Testing Strategy
- Write tests for each error case
- Use typed throws to ensure test coverage
Performance Considerations
Runtime Impact
- Typed throws eliminate type erasure overhead
- Direct dispatch instead of protocol witness tables
- Reduced allocation for error boxing
Binary Size
- Smaller code generation for error handling
- Reduced generic instantiations
- More efficient error propagation paths
Conclusion
Typed throws in Swift 6 represent a significant evolution in error handling, bringing Swift closer to truly type-safe systems programming. The feature enhances API clarity, improves performance, and provides compile-time guarantees that reduce runtime errors.
Top comments (1)
Functions explicitly declare which error types they throw
Compile-time guarantees about error types