Error Handling Best Practices?

In Python, it is a best practice to try and except errors, versus to check if a value exists. For example, it is considered bad practice if I was to do:

if my_dict['name']:
    name = my_dict['name']

The best practice in Python is instead to try and except the assignment of the name variable above. It is more efficient in the Python language (faster). So this is the best practice:

try:
    name = my_dict['name']
except KeyError:
    name = ''

Is there a similar practice in Swift? I ask, because in the iOS Foundations, Module 2 Lesson 4 Challenge, my Preview crashes. It crashes, when I try to add 1 to every element in a List that is empty. So it crashes, when the second button is pressed; the method is called, and it tries to execute my for loop.

Here is the loop itself:

for i in 0...currentNumberArray.count-1 {
    // Access the element in the array, then add 1 to it
    currentNumberArray[i] += 1
 }

I could solve this by checking to make sure my array has elements first like this:

if currentNumberArray.count > 0 {
    for i in 0...currentNumberArray.count-1 {
        // Access the element in the array, then add 1 to it
        currentNumberArray[i] += 1
    }
}

Otherwise, I tried to except it by following Swift 5 Try Catch Syntax and Error Handling (Code Examples), but haven’t had much luck. In other words, it doesn’t work…

do {
            // Loop through each element in the array, and add one.
            for i in 0...currentNumberArray.count-1 {
                // Access the element in the array, then add 1 to it
                currentNumberArray[i] += 1
            }
        } catch {
            print("Caught error. No numbers in array")
        }

It gives a warning: ‘catch’ block is unreachable because no errors are thrown in ‘do’ block. Also, in Python, it is bad practice to blanked Except everything. So we should instead catch the specific error. For that, the error message from the debugger is this:
Swift/x86_64-apple-ios-simulator.swiftinterface:5670: Fatal error: Range requires lowerBound <= upperBound 2021-07-20 22:06:22.269136-0400 Lesson4-Challenge[51025:3177991] Swift/x86_64-apple-ios-simulator.swiftinterface:5670: Fatal error: Range requires lowerBound <= upperBound (lldb)

Also, in Python, I would want to log the full stack trace. So catch the particular error, even if it was a blanket Exception:

try: 
   name = my_dict['name']
Except as e:
     print(f'Unexpected error occurred. Full stack: {e}')

The above would print out:
f'Unexpected error occurred. Full stack: KeyError: 'name'

Is there something similar in Swift?

1 Like

Aside from the usual try catch theres also the guard statement that you might want to explore

1 Like

do {} catch {} is sort of the equivalent of exceptions in Swift, but as you see it doesn’t always apply. You have to do something inside the do block that has the potential to throw an error, indicated by the throws keyword on the function signature and use of the try keyword when calling it. It doesn’t apply to just any old random error that could occur. Swift tends to use other means of handling errors, like Optionals.

Something like:

for i in 0...currentNumberArray.count-1 {
    // Access the element in the array, then add 1 to it
    currentNumberArray[i] += 1
}

just isn’t very idiomatic Swift. This is better and more “Swifty”:

currentNumberArray.map { $0 + 1 }

This also means that nothing will happen if your array is empty, unlike with your original example, which crashes.

An example of using an Optional for error handling would be your code accessing a key in a dictionary:

//Python:
try: 
   name = my_dict['name']
Except as e:
     print(f'Unexpected error occurred. Full stack: {e}')

//Swift:
let name = myDict["name"] //returns an Optional if key not found
//you can use nil-coalescing to provide a value if key not found
let name = myDict["name"] ?? "No name found" 
//or you can test for the optional later in your code where you use it
if let name = myDict["name"] { ... }
//or:
if name != nil { ... }

//or you can provide a default value in the subscript: 
let name = myDict["name", default: "No name found"] //returns the default value if key not found

I recommend reading the section on Error Handling in the docs and Error Handling Rationale and Proposal in the Swift source repository.

3 Likes

Hey thanks so much for this quick reply and thorough answer! Appreciate the reference to more “Swifty” code… In Python, I try to use their “Pythonic” way. However, when I tried to implement that section:

func addOneToNumberArray() {
   currentNumberArray.map { $0 + 1 }
}

It shows the warning: Result of call to 'map' is unused. Thankfully, this prevents the code from crashing. However, it doesn’t add 1 to all of the list of numbers in the view either.

Thanks for providing all those different ways to handle that other condition as well! The “nil-coalescing” way is super interesting, I’m not used to seeing ?? that in a language, and it’s clean how it’s this or that. Meanwhile, the last example with the subscript is also quite cool.

Thanks for taking the time to write all this as well as those two links!

Hey Francis, thanks for this link! I was so hopeful guard would be the clean/ useful code I needed. However, that’s not the case :confused: or at least I haven’t figured it out yet for use in a loop, where it updates properties for this struct.

You could just assign the array to itself:

func addOneToNumberArray() {
   currentNumberArray = currentNumberArray.map { $0 + 1 }
}
1 Like

Boom!

Yes, that’s perfect. Before, I was about to check if the array included any elements with an if statement, but this is so much cleaner. Thanks!

@RedFox1, you can use guard in a loop like this:

let rng = 0...10
for index in rng {
    guard index.isMultiple(of: 2) else {
        continue
    }
    print(index)
}

This will loop over the range 0 to 10 and check if the current value of index is a multiple of 2. If it is not, the continue keyword causes the loop to jump to the next iteration without executing any of the remaining code inside. (break, on the other hand, will cause you to completely drop out of the loop entirely.)

You can think of it as the equivalent of:

let rng = 0...10
for index in rng {
    if index.isMultiple(of: 2) {
        print(index)
    }
}

guard is very useful to avoid nested if clauses and to provide a way to exit a block of code early if necessary conditions aren’t met.