Lesson 12 / 25
Error Handling
Throw, propagate and handle errors with do-catch, try?, Result and typed throws.
Errors as part of the signature
Swift functions that can fail are marked throws, and every call to them is marked try, so failure points are visible in code. Errors are values conforming to the Error protocol, usually enums with associated values. Handle them with do/catch, matching specific cases (catch PaymentError.declined(let reason)), or propagate them by making the caller throws too. try? converts a failure into nil, convenient when you only care whether something worked, and try! crashes on failure, so reserve it for cases that can only fail through programming errors. Swift 6 added typed throws: func charge() throws(PaymentError), which lets callers know the exact error type and switch over it exhaustively, useful in libraries and embedded code (plain throws remains the common default). Result<Success, Failure> stores a success or failure as a value, handy for callback APIs and caching outcomes. defer guarantees cleanup however a scope exits. Swift errors are not exceptions: there is no expensive stack unwinding, and throwing is a normal return path.
Throwing, catching and typed throws
Callers see every failure point through try.
import Foundation
enum PaymentError: Error {
case invalidAmount
case declined(reason: String)
case networkUnavailable
}
func charge(_ amount: Double, cardToken: String) throws(PaymentError) -> String { // typed throws (Swift 6)
guard amount > 0 else { throw .invalidAmount }
if cardToken.hasPrefix("blocked") { throw .declined(reason: "Card blocked by issuer") }
return "pay_" + UUID().uuidString.prefix(8)
}
do {
let paymentId = try charge(499, cardToken: "tok_abc")
print("Paid: \(paymentId)")
} catch .declined(let reason) {
print("Declined: \(reason)")
} catch {
print("Payment failed: \(error)") // error is PaymentError here
}
let maybeId = try? charge(-5, cardToken: "tok") // nil on failure
func loadConfig(at path: String) throws -> String {
let file = try openFile(path)
defer { closeFile(file) } // runs however the scope exits
return try readAll(file)
}try? hides the reason
try? discards the error, so a failure is indistinguishable from an empty result. Use it when the reason genuinely does not matter, and log or handle the error otherwise.
Quick check: What does try? do with a thrown error?
- Converts the result to nil when an error is thrown
- Crashes the app
- Rethrows it automatically
- Prints it
Answer
Converts the result to nil when an error is thrown — try? turns failures into nil optionals, discarding the error value.