Skip to Content
Course content

113: Typed Throws (Swift 6)

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

For a long time, Swift's error handling has had a bit of a "blind spot." When you mark a function with throws, you're telling the compiler that something might go wrong, but you aren't specifying what. You might have a beautifully defined PaymentError enum with cases for insufficientFunds and expiredCard, but as far as the function signature is concerned, it just throws a generic Error. It's like a warning sign that says "Danger," but doesn't tell you if you're dealing with a landslide or a swarm of bees.

The friction of the generic catch

I used to write my API clients like this for years because it was the only way. Let's look at a typical payment processing function. Even though I know exactly what errors this function produces, the signature is vague:

enum PaymentError: Error {
    case insufficientFunds
    case expiredCard
    case networkFailure
}

func processPayment(amount: Decimal) throws {
    // Imagine some logic here that throws PaymentError.insufficientFunds
    throw PaymentError.insufficientFunds
}

do {
    try processPayment(amount: 100.0)
} catch {
    // Here is the problem: 'error' is of type 'Error', not 'PaymentError'
    if let paymentError = error as? PaymentError {
        print("Payment failed: \(paymentError)")
    } else {
        print("An unknown error occurred")
    }
}

The friction here is that as? PaymentError cast. It's boilerplate we've all just accepted. We know the function only throws PaymentError, but the compiler doesn't. This means you lose autocomplete inside the catch block, and you're forced to write defensive code for a "generic" error that you know, logically, can't happen in this specific call.

Bringing types to the throw

Swift 6 finally lets us close this gap with Typed Throws. Instead of just throws, we can now specify the exact type: throws(PaymentError). It seems like a small syntax change, but it fundamentally changes how the compiler views the error path.

func processPayment(amount: Decimal) throws(PaymentError) {
    throw PaymentError.insufficientFunds
}

do {
    try processPayment(amount: 100.0)
} catch {
    // Magic happens here: 'error' is automatically inferred as 'PaymentError'
    // No casting required.
    switch error {
    case .insufficientFunds:
        print("Please top up your account.")
    case .expiredCard:
        print("Please update your card details.")
    case .networkFailure:
        print("Check your connection.")
    }
}

Now, the catch block is type-safe. If I try to handle an error that isn't part of PaymentError, the compiler will let me know. More importantly, I don't have to play "guess the type" with a cast. It makes the contract of the function explicit: "I might fail, and if I do, it will be for one of these specific reasons."

When to stay fuzzy

You might be wondering if you should go back and change every throws in your codebase to a typed version. I wouldn't recommend that. Typed throws are great for internal logic and specific domain errors, but they can become a burden when you're composing many different layers of a system.

If you have a high-level "Coordinator" function that calls a database service, a network service, and a file system manager, that function might potentially throw three or four different error types. Trying to create a "MegaError" enum to wrap them all just to satisfy a typed throw signature can lead to a lot of tedious mapping code. In those cases, the classic, untyped throws is actually the better tool because it acts as a common denominator for any type that conforms to Error.

I generally follow this rule of thumb: use typed throws for "leaf" functions—the ones doing the actual work—and use untyped throws for "orchestration" functions that just pass errors up the chain. It gives you type safety where you need it most without fighting the compiler when you're just routing errors to the UI.




📋 Practical Task

Refactoring the Vault Access Controller

You are working on a secure storage module. Currently, the Vault class uses untyped throws, forcing the UI layer to use clumsy casting to figure out why access was denied. Your task is to refactor the code to use Swift 6 Typed Throws.

Requirements:

  • Modify the openVault function signature to throw only VaultError.
  • Update the do-catch block in the main execution area to remove the as? VaultError cast.
  • Use a switch statement inside the catch block to handle the specific cases of VaultError directly.
enum VaultError: Error {
    case wrongPin
    case vaultLocked
    case biometricFailure
}

struct Vault {
    // TODO: Change this signature to use Typed Throws
    func openVault(pin: Int) throws {
        if pin != 1234 {
            throw VaultError.wrongPin
        }
        // Simulate a lock
        throw VaultError.vaultLocked
    }
}

let myVault = Vault()

do {
    try myVault.openVault(pin: 0000)
} catch {
    // TODO: Remove the manual casting and handle the error type-safely
    if let error = error as? VaultError {
        print("Access denied: \(error)")
    } else {
        print("Unknown error")
    }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.