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

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 UploadError that conforms to the Error protocol.
  • Include these cases:
    • fileTooLarge: Should include an associated value maxSizeMB (Double).
    • unsupportedFormat: Should include an associated value extension (String).
    • diskFull: No associated value needed.
  • Write a function called validateUpload(sizeMB: Double, fileExtension: String) throws.
  • In the function:
    • Throw .fileTooLarge if the size is over 100MB.
    • Throw .unsupportedFormat if the extension is not "jpg", "png", or "pdf".
    • (Optional) Simulate a .diskFull error randomly or based on a boolean flag.
  • Call this function inside a do-catch block. Use specific catch statements to print a custom message for the size and format errors, using their associated values in the print statement.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.