DEV Community

ArshTechPro
ArshTechPro

Posted on

Swift 6 Error Handling: Typed Throws Explained

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
}
Enter fullscreen mode Exit fullscreen mode

Typed Throws (Swift 6):

func fetchUser(id: String) throws(NetworkError) -> User {
    // Can only throw NetworkError types
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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")
    }
}
Enter fullscreen mode Exit fullscreen mode

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!
    }
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
arshtechpro profile image
ArshTechPro

Functions explicitly declare which error types they throw
Compile-time guarantees about error types