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
30: Custom Error Types
Up until now, we've touched on how to handle errors, but we've mostly been dealing with the basics. In a real production app, simply knowing that "something went wrong" isn't helpful for the user—or for you, the developer, when you're staring at a crash log at 2 AM. You need to know exactly what failed so you can respond appropriately.
Why generic errors aren't enough
Imagine we're building a simple Digital Wallet for a coffee shop. If a payment fails, we can't just throw a generic error. There is a huge difference between "Your card is expired" (which requires the user to update their settings) and "You don't have enough money" (which requires them to top up their balance). If we use a generic error, we can't tell the UI which message to show.
To fix this, we use a custom error type. In Swift, the most elegant way to do this is with an enum that conforms to the Error protocol. Since Error is an empty protocol, any enum can adopt it without needing to implement any specific methods.
Modeling our wallet failures
I'll start by defining the specific things that can go wrong when processing a payment. I want to keep this tight and focused.
enum WalletError: Error {
case insufficientFunds
case cardExpired
case networkFailure
case invalidAmount
}
Now we have a type-safe way to describe our failures. Instead of passing strings around—which is a recipe for typos and bugs—we have a concrete set of cases the compiler can help us track.
Throwing errors from the payment logic
Now, let's put this into a function. I'll create a Wallet class that tracks a balance and attempts to process a purchase. Notice how I use the throws keyword to signal that this function is "dangerous."
class Wallet {
var balance: Double = 5.00
func processPayment(amount: Double) throws {
if amount <= 0 {
throw WalletError.invalidAmount
}
if amount > balance {
throw WalletError.insufficientFunds
}
balance -= amount
print("Payment successful! Remaining balance: $\(balance)")
}
}
Correcting a missed opportunity for detail
I just realized I made a classic "engineer mistake" here. I'm throwing .insufficientFunds, but the user has no idea how much more money they actually need. Just telling them "you're broke" is a bad user experience.
The beauty of Swift enums is that they can have associated values. I'm going to go back and rewrite the enum so the error itself carries the missing amount. This allows the calling code to be much more helpful.
enum WalletError: Error {
case insufficientFunds(amountNeeded: Double) // Added associated value
case cardExpired
case networkFailure
case invalidAmount
}
Now, I'll update the processPayment logic to calculate that difference:
if amount > balance {
let missing = amount - balance
throw WalletError.insufficientFunds(amountNeeded: missing)
}
Pinpointing the problem in the catch block
The real power of custom errors shows up when we actually call the function. Because we used an enum, we can use pattern matching in our catch blocks to handle each error differently. This is where you decide if the app should show an alert, trigger a top-up screen, or just log a warning.
let myWallet = Wallet()
do {
try myWallet.processPayment(amount: 12.50)
} catch WalletError.insufficientFunds(let missing) {
print("You're short $\(missing). Please add funds to your wallet.")
} catch WalletError.invalidAmount {
print("The payment amount must be greater than zero.")
} catch {
print("An unexpected error occurred: \(error).")
}
By being specific, I've turned a generic failure into a guided user experience. You're not just catching an error; you're diagnosing a problem.
📋 Practical Task
Build a File Upload Validator
You are building a module for a cloud storage app. Your task is to create a custom error type and a validation function to ensure files are uploaded correctly.
- Create an enum called
UploadErrorthat conforms to theErrorprotocol. - Include these cases:
fileTooLarge: Should include an associated valuemaxSizeMB(Double).unsupportedFormat: Should include an associated valueextension(String).diskFull: No associated value needed.
- Write a function called
validateUpload(sizeMB: Double, fileExtension: String) throws. - In the function:
- Throw
.fileTooLargeif the size is over 100MB. - Throw
.unsupportedFormatif the extension is not "jpg", "png", or "pdf". - (Optional) Simulate a
.diskFullerror randomly or based on a boolean flag.
- Throw
- Call this function inside a
do-catchblock. Use specificcatchstatements to print a custom message for the size and format errors, using their associated values in the print statement.
There are no comments for now.