Skip to Content
Course content

28: Error Handling with throws and try

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've noticed a recurring pattern when developers first hit error handling in Swift: they treat throw as if it's just a fancy return statement. They think that by marking a function as throws, they're just "tagging" it for later, and that adding try before the call is a polite suggestion to the compiler.

Thinking 'try' is just a label

Here is the mistake. Someone writes a function to process a payment, marks it as throws, and then tries to call it like this:

func processPayment(amount: Double) throws {
    if amount <= 0 { throw PaymentError.invalidAmount }
    // process payment...
}

// The misconception: "I'll just put 'try' here and it'll be fine."
try processPayment(amount: -5.0) 

If you try to compile this in a standard function, the compiler will stop you cold. It'll tell you that try is used in a place where an error isn't handled. The misconception is that try simply "allows" the error to happen. In reality, try is a signal that execution might stop right here. If the function throws, the code immediately below that line will never run. You can't just ignore that possibility; you have to tell Swift exactly what to do when the wheels fall off.

Defining your own failure states

Before you can throw something, you need to define what "something" is. In Swift, we do this using enums that conform to the Error protocol. I always recommend being as specific as possible here. Don't just create a generic AppError; tell me exactly what went wrong.

enum VendingMachineError: Error {
    case itemOutOfStock
    case insufficientFunds(coinsNeeded: Int)
    case invalidSelection
}

Notice the insufficientFunds case. Since enums can have associated values, we can pass the exact amount the user is missing back to the caller. This is way more useful than a boolean false.

The contract of 'throws' and 'do-catch'

When you mark a function with throws, you're signing a contract. You're telling anyone who uses your function, "I might give you a result, or I might blow up in your face."

func vend(item: String, coins: Int) throws {
    if item == "Snickers" && coins < 10 {
        throw VendingMachineError.insufficientFunds(coinsNeeded: 10 - coins)
    }
    if item == "Pretzels" {
        throw VendingMachineError.itemOutOfStock
    }
    print("Dispensing \(item)...")
}

To call this, you use a do-catch block. I like to think of the do block as the "happy path"β€”the place where you write the code as if everything will work perfectly. The catch blocks are your safety nets.

do {
    try vend(item: "Snickers", coins: 5)
    print("Enjoy your snack!") 
} catch VendingMachineError.insufficientFunds(let needed) {
    print("You need \(needed) more coins, buddy.")
} catch {
    print("Something else went wrong: \(error)")
}

If vend throws, the print("Enjoy your snack!") line is skipped entirely. The execution jumps straight to the matching catch block. If no specific case matches, the final catch block acts as a catch-all.

The shortcuts: try? and try!

Sometimes, a full do-catch block is overkill. Maybe you don't actually care why it failed, just that it did fail. That's where try? comes in. It converts the result into an optional. If the function throws, it returns nil instead of crashing your program.

let result = try? vend(item: "Pretzels", coins: 100)
// result is now nil because it threw itemOutOfStock

Then there's try!. Use this with extreme caution. It's you telling the compiler, "I am 100% certain this will never fail, and if it does, feel free to crash the entire app." I rarely use this in production, except when loading a bundled resource that I know exists in the app package.




πŸ“‹ Practical Task

Build a Robust Password Validator

You need to create a password validation system that doesn't just return true or false, but tells the user exactly why their password was rejected.

Requirements:

  • Create an enum called PasswordError that conforms to Error. It should have cases for:
    • tooShort (include an associated value for the minimum length required)
    • missingSpecialCharacter
    • containsSpaces
  • Write a function validatePassword(_ password: String) throws.
    • Throw .tooShort if the password is less than 8 characters.
    • Throw .containsSpaces if the password contains any whitespace.
    • Throw .missingSpecialCharacter if the password doesn't contain at least one symbol (e.g., !, @, #, $).
  • Implement a do-catch block that tests three different invalid passwords and prints a user-friendly message for each specific error.
  • Use try? to attempt to validate a final password and print whether the attempt succeeded or failed using a simple "Success" or "Failure" message.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.