Swift
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
113: Typed Throws (Swift 6)
For a long time, Swift's error handling has had a bit of a "blind spot." When you mark a function with throws, you're telling the compiler that something might go wrong, but you aren't specifying what. You might have a beautifully defined PaymentError enum with cases for insufficientFunds and expiredCard, but as far as the function signature is concerned, it just throws a generic Error. It's like a warning sign that says "Danger," but doesn't tell you if you're dealing with a landslide or a swarm of bees.
The friction of the generic catch
I used to write my API clients like this for years because it was the only way. Let's look at a typical payment processing function. Even though I know exactly what errors this function produces, the signature is vague:
enum PaymentError: Error {
case insufficientFunds
case expiredCard
case networkFailure
}
func processPayment(amount: Decimal) throws {
// Imagine some logic here that throws PaymentError.insufficientFunds
throw PaymentError.insufficientFunds
}
do {
try processPayment(amount: 100.0)
} catch {
// Here is the problem: 'error' is of type 'Error', not 'PaymentError'
if let paymentError = error as? PaymentError {
print("Payment failed: \(paymentError)")
} else {
print("An unknown error occurred")
}
}
The friction here is that as? PaymentError cast. It's boilerplate we've all just accepted. We know the function only throws PaymentError, but the compiler doesn't. This means you lose autocomplete inside the catch block, and you're forced to write defensive code for a "generic" error that you know, logically, can't happen in this specific call.
Bringing types to the throw
Swift 6 finally lets us close this gap with Typed Throws. Instead of just throws, we can now specify the exact type: throws(PaymentError). It seems like a small syntax change, but it fundamentally changes how the compiler views the error path.
func processPayment(amount: Decimal) throws(PaymentError) {
throw PaymentError.insufficientFunds
}
do {
try processPayment(amount: 100.0)
} catch {
// Magic happens here: 'error' is automatically inferred as 'PaymentError'
// No casting required.
switch error {
case .insufficientFunds:
print("Please top up your account.")
case .expiredCard:
print("Please update your card details.")
case .networkFailure:
print("Check your connection.")
}
}
Now, the catch block is type-safe. If I try to handle an error that isn't part of PaymentError, the compiler will let me know. More importantly, I don't have to play "guess the type" with a cast. It makes the contract of the function explicit: "I might fail, and if I do, it will be for one of these specific reasons."
When to stay fuzzy
You might be wondering if you should go back and change every throws in your codebase to a typed version. I wouldn't recommend that. Typed throws are great for internal logic and specific domain errors, but they can become a burden when you're composing many different layers of a system.
If you have a high-level "Coordinator" function that calls a database service, a network service, and a file system manager, that function might potentially throw three or four different error types. Trying to create a "MegaError" enum to wrap them all just to satisfy a typed throw signature can lead to a lot of tedious mapping code. In those cases, the classic, untyped throws is actually the better tool because it acts as a common denominator for any type that conforms to Error.
I generally follow this rule of thumb: use typed throws for "leaf" functions—the ones doing the actual work—and use untyped throws for "orchestration" functions that just pass errors up the chain. It gives you type safety where you need it most without fighting the compiler when you're just routing errors to the UI.
📋 Practical Task
Refactoring the Vault Access Controller
You are working on a secure storage module. Currently, the Vault class uses untyped throws, forcing the UI layer to use clumsy casting to figure out why access was denied. Your task is to refactor the code to use Swift 6 Typed Throws.
Requirements:
- Modify the
openVaultfunction signature to throw onlyVaultError. - Update the
do-catchblock in themainexecution area to remove theas? VaultErrorcast. - Use a
switchstatement inside thecatchblock to handle the specific cases ofVaultErrordirectly.
enum VaultError: Error {
case wrongPin
case vaultLocked
case biometricFailure
}
struct Vault {
// TODO: Change this signature to use Typed Throws
func openVault(pin: Int) throws {
if pin != 1234 {
throw VaultError.wrongPin
}
// Simulate a lock
throw VaultError.vaultLocked
}
}
let myVault = Vault()
do {
try myVault.openVault(pin: 0000)
} catch {
// TODO: Remove the manual casting and handle the error type-safely
if let error = error as? VaultError {
print("Access denied: \(error)")
} else {
print("Unknown error")
}
}There are no comments for now.