Lesson 88Error Handling
Assertions & Preconditions
Catching Programmer Errors
Assertions and preconditions help you catch bugs during development by checking conditions that should always be true. They're different from error handling (which handles expected failures).
Comparison
| Function | Debug | Release | Use Case |
|---|---|---|---|
| assert() | Yes | No | Dev checks |
| precondition() | Yes | Yes | Critical checks |
| fatalError() | Yes | Yes | Unrecoverable |
When to Use Each
assert()
Development sanity checks
precondition()
API contract enforcement
fatalError()
Unimplemented code paths
The Never Type
Functions that never return (like fatalError()) have return type Never. The compiler knows code after them is unreachable.
Important Warning
Don't use assertions for user errors!
Use throws for recoverable errors (invalid input, network failures). Assertions are for programmer bugs that should never happen in correct code.
main.swift
// ASSERTIONS & PRECONDITIONS
// Check conditions during development and runtime
// ===== ASSERTIONS =====
// Only checked in debug builds (removed in release)
let age = 25
// Basic assertion
assert(age >= 0, "Age cannot be negative")
// Assertion without message
assert(age < 150)
// assertionFailure - always fails
func processItem(at index: Int, in array: [Int]) {
if index < 0 {
assertionFailure("Index cannot be negative")
}
// Process item...
}
// ===== PRECONDITIONS =====
// Checked in both debug AND release builds
func divide(_ a: Int, by b: Int) -> Int {
precondition(b != 0, "Cannot divide by zero")
return a / b
}
// preconditionFailure - always fails, even in release
func handleUnexpectedCase(_ value: Int) -> Never {
preconditionFailure("Unexpected value: \(value)")
}
// ===== FATAL ERROR =====
// Always crashes, both debug and release
// Returns Never type
func mustImplement() -> Never {
fatalError("This method must be overridden")
}
// Common use: Required overrides
class BaseClass {
func requiredMethod() {
fatalError("Subclasses must implement requiredMethod()")
}
}
// Common use: Unimplemented code paths
enum Status {
case active, inactive, pending
}
func handleStatus(_ status: Status) {
switch status {
case .active:
print("Active")
case .inactive:
print("Inactive")
case .pending:
fatalError("Pending status not yet implemented")
}
}
// ===== COMPARISON TABLE =====
// | Function | Debug | Release | Use Case |
// |---------------------|-------|---------|------------------------------|
// | assert() | Yes | No | Development checks |
// | assertionFailure() | Yes | No | Debug-only failure points |
// | precondition() | Yes | Yes | Critical runtime checks |
// | preconditionFailure()| Yes | Yes | Critical failure points |
// | fatalError() | Yes | Yes | Unrecoverable errors |
// ===== PRACTICAL EXAMPLES =====
// 1. Array bounds checking (development)
func getElement<T>(from array: [T], at index: Int) -> T {
assert(index >= 0 && index < array.count, "Index out of bounds")
return array[index]
}
// 2. Required initialization
class ViewController {
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// 3. Protocol requirements
protocol DataSource {
func numberOfItems() -> Int
}
class DefaultDataSource: DataSource {
func numberOfItems() -> Int {
assertionFailure("Override this method")
return 0
}
}
// 4. Unreachable code
func processResult(_ result: Result<Int, Error>) -> Int {
switch result {
case .success(let value):
return value
case .failure:
preconditionFailure("This should never fail")
}
}
// 5. API contract enforcement
func setVolume(_ level: Int) {
precondition(level >= 0 && level <= 100, "Volume must be 0-100")
// Set volume...
}
// ===== NEVER TYPE =====
// Functions that never return
func infiniteLoop() -> Never {
while true { }
}
func crash() -> Never {
fatalError("Crash!")
}
// Can be used in switch exhaustiveness
func getValue(_ optional: Int?) -> Int {
switch optional {
case .some(let value):
return value
case .none:
fatalError("Value was nil") // Never returns
}
}
// ===== DEBUGGING TIPS =====
// 1. Use assert for development-time sanity checks
// 2. Use precondition for critical runtime invariants
// 3. Use fatalError for programmer errors, not user errors
// 4. Never use these for recoverable errors - use throws instead!Try It Yourself!
Create a Stack data structure and use preconditions to enforce that pop() is never called on an empty stack!