When to use Swift Result Type vs Throws (and why it matters)

After explaining error handling using do-try-catch, I thought it would be irresponsible not to share my daily notes on the swift Result type. It’s very simple to return result from a function and handle errors especially if you want to handle both cases, success & failure.

Throws is great for immediately stopping execution and jumping to the catch block. In contrast, Result is better for continuing with execution and the error becomes a piece of data. It’s like a package arriving at your doorstep.

Throws is very clean using guard & try, however Result can get a bit nested using switch.

Throws is best for sync & async/await and Result is better in closures & passing errors between threads.

If you’re thinking to yourself this shouldn’t happen, stop everything then go with throws but if you say here is the outcome of the attempt good or bad then use Result

I’m going to show you how to use result with a completion handler.

mutating func withdraw(amount: Double, completion: @escaping (Result<Double, BankError>) -> Void) { }

In our withdraw function, we added a closure.

@escaping means wait for a trigger of some sort later. The function runs and returns.

@escaping puts the closure in queue and waits to be called.

Closures are non escaping by default so if you don’t add @escaping, the closure will run before the function returns.

Next Result<Success, Failure>

I want you to think of Result as package and inside the large box you have two smaller boxes. 1 box is labeled success and the other box is labeled failure.

If you open the large box and then open the box labeled success, you will find a Double.

If you open the large box and then open the box labeled failure, you will find a BankError.

In swift we call this syntax a generic. It allows us to define a behavior without getting bogged down in the data type which is why in our closure we can write Result<Double, BankError>

If this is your first time landing on this post without reviewing any previous posts then BankError enumeration will make more sense if you read this post: Stop Guessing Why Code Fails: Using Swift Enums for Detailed Error Handling.

The last part of this closure is -> Void and we’re just saying hey we don’t need anything else back. Our closure takes data in but doesn’t return anything out.

Below is the source code that we’re going refactor to use Result

enum BankError: Error {
    case invalidAmount
    case insufficientFunds(missingAmount: Double)
}


struct BankAccount {

var owner:String
private(set) var balance:Double

mutating func depositIntoAccount(amount: Double){

 balance += amount


}

mutating func withdraw(amount: Double) throws {

guard amount > 0 else {
    throw BankError.invalidAmount

}
guard balance >= amount else {

    let balanceShortBy = amount - balance
    throw BankError.insufficientFunds(missingAmount: balanceShortBy)
}

balance -= amount
print("Success! Remaining balance: \(balance)")

}


}


var myAccount = BankAccount(owner: "Jim", balance: 200.0)

myAccount.depositIntoAccount(amount: 20)
print(myAccount.balance)

do {
  try myAccount.withdraw(amount: 1200)
    
} catch BankError.invalidAmount {
    
    print("Please enter a positive amount")
    
    
} catch BankError.insufficientFunds(missingAmount: let amountNeeded){
    
    
    print("Insufficient Funds: You need \(amountNeeded) to complete the withdrawal")
    
} catch {
    
    print("Unexpected error")
}
            

print(myAccount.balance)

So lets update withdraw function to have a closure and remove throws

mutating func withdraw(amount: Double, completion: @escaping (Result<Double, BankError>) -> Void)  {

guard amount > 0 else {
    completion(.failure(.invalidAmount))
                return
    

}
guard balance >= amount else {

    let balanceShortBy = amount - balance
    completion(.failure(.insufficientFunds(missingAmount: balanceShortBy)))
                return
   
}

balance -= amount
        completion(.success(balance))

}

Add completion for the failures after each guard statement and after balance deduction for success.

Now lets handle the Result when money is being withdrawn. We’re going to use a switch statement to handle each case.

Xcode is always quick to scream about an error.A switch statement has to handle every case which is a good thing so if you handle lets say .success and not failure you will get this error.


error: Switch must be exhaustive
– means: not handling every case of a switch statement


var myAccount = BankAccount(owner: "Jim", balance: 200.0)

myAccount.depositIntoAccount(amount: 20)
    print(myAccount.balance)
    
    
myAccount.withdraw(amount: 1200){ result in
    
    
    switch result {
        
    case .success(let newBalance):
        print("Your new balance is: \(newBalance)")
        
    case .failure(.insufficientFunds(let missingAmount)):
        print("You need \(missingAmount) dollars deposited into your account to complete this transaction.")
        
    case .failure(.invalidAmount):
        
        print("Please enter a positive amount")
    }
    
}

print(myAccount.balance)

Notice how we switch through result. Our result has case .success and case .failure. We store our success balance in newBalance.

Each failure has cases for our BankError which we handle. We access failure by .failure and then access the specific cases of BankError .insufficientFunds and .invalidAmount.

.insufficientFunds stores the missingAmount in missingAmount which we access in our print statement.

If you’re in playground, you may notice that none of your print statements are showing. We need to let playground know to keep running by importing PlaygroundSupport and setting needsIndefiniteExecution = true

import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true

Now you should be able to withdraw money and see the print statements. Play around with the numbers in the withdraw function and analyze the results.

Below is the full source code

import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true

enum BankError: Error {
    case invalidAmount
    case insufficientFunds(missingAmount: Double)
}


struct BankAccount {

var owner:String
private(set) var balance:Double

mutating func depositIntoAccount(amount: Double){

 balance += amount


}

    mutating func withdraw(amount: Double, completion: @escaping (Result<Double, BankError>) -> Void)  {

guard amount > 0 else {
    completion(.failure(.invalidAmount))
                return
    

}
guard balance >= amount else {

    let balanceShortBy = amount - balance
    completion(.failure(.insufficientFunds(missingAmount: balanceShortBy)))
                return
   
}

balance -= amount
        completion(.success(balance))

}


}


var myAccount = BankAccount(owner: "Jim", balance: 200.0)

myAccount.depositIntoAccount(amount: 20)
    print(myAccount.balance)
    
    
myAccount.withdraw(amount:1200){ result in
    
    
    switch result {
        
    case .success(let newBalance):
        print("Your new balance is: \(newBalance)")
        
    case .failure(.insufficientFunds(let missingAmount)):
        print("You need \(missingAmount) dollars deposited into your account to complete this transaction.")
        
    case .failure(.invalidAmount):
        
        print("Please enter a positive amount")
    }
    
}

print(myAccount.balance)



This is part of my daily Swift notes on writing safer, more predictable code.

Master Swift, One Tip a Day

Join other developers getting one bite-sized Swift lesson delivered to their inbox every morning.

🚀 Awesome! Check your inbox for your first tip.
Something went wrong. Try again.

1 thought on “When to use Swift Result Type vs Throws (and why it matters)”

  1. Pingback: Stop Guessing Why Code Fails: Using Swift Enums for Detailed Error Handling – Swift Owls

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top