When is it *good* for your App to crash?

Most of the time, a crashing app is the very last thing you want. Obviously.

But sometimes you really do want your app to crash to enforce correct usage of your APIs.

What’s worse than a crash?

Crashes are obviously bad – it’s disruptive to your user, they might lose data or work in progress, it makes them feel frustrated and less likely to open your app again, and it just smells like bad quality.

But there’s something worse than a crash: undefined behaviour.

Our programs are carefully thought through, and crafted so that the right input data is manipulated, used for calculations, stored, outputted, and otherwise trusted. But this also makes them brittle. Whether you realize it or not, all of these complex behaviours only work when you constrain your code to be used with some predefined, known range of inputs.

When your code is given unexpected types of data, you can’t control what will happen – it’s outside of what you designed and tested for. You could end up accidentally corrupting user data, opening security holes, or (the most likely) providing inaccurate and confusing information to the user.

In some cases, these things are worse than crashing.

  • What if your banking app tells the user they have more money than they really have and then they suddenly can’t pay rent!
  • What if your app ends up in a state where none of the buttons do anything and you can’t navigate out of that screen?

There are of course others, but these user experiences might actually be *worse* than crashing. So crash! The user knows something went wrong, and needs to restart the app to fix it.

Crashing as a Development Tool

Crashing is also extremely useful in teaching developers how to use your APIs.

A (contrived) example

Lets say you’re writing the official calculator app for a university. It’s very important that it do math correctly, because all of this university’s students will be forced to use this app for all of their exams.

Ok so your inputs are numbers and operators.

Let’s imagine you had some calculator UI, with logic that looks something like this:

We could write a custom UITextField subclass that only supported numbers that could be converted into a float.

class CalculatorUI {
    @IBOutlet var input1: NumericTextField!
    @IBOutlet var input2: NumericTextField!
    @IBOutlet var result: NumericTextField!
    
    /// Takes the inputs, adds them, and updates the result TextField.
    @IBAction func didPressAddButton() {
        result.floatValue = input1.floatValue + input2.floatValue
    }
}

This super clean, simple class relies on pushing some of the complication into the NumericTextField:

// Basic Implementation
class NumericTextField: UITextField {

    var floatValue: Float {
        get {
            if let acceptableValue = self.text as? Float {
                return acceptableValue
            } else {
                // What do we return here?
                return 0
            }
        }
        set { self.text = "\(newValue)" }
    }
}

There are a couple problems with our basic implementation:

  1. We always return 0 if the text can’t be converted into a float. If a student somehow types non-float values like "two" and "two" as their inputs, they might not realize this isn’t supported, and actually believe that their answer is 0! This answer is not correct! AHH
  2. The NumericTextField class design encourages misuse! There is nothing preventing us from trying to access the .text property directly, but the whole point of this custom text field is to restrict the types to be floats.

Clearly, the real solution here is to show some error message that tells the user to only type numbers, or better yet to use a numeric keyboard style so enforce that they can’t enter alphabetical characters.

But we still want to improve this text field’s implementation.

Crashing is a hint to the developer!

The key thing here is that both of the problems described above are developer errors. They are misusing the API. We want to catch these mistakes as early into the development process as possible. And the best way to call attention to them is to crash.

Yep. We’re going to crash. On purpose.

Solution #1: Catch the invalid text field values

If the text field’s getter method is unable to convert the text to a Float, that is because the developer has allowed the user to input letters.

class NumericTextField: UITextField {

    var floatValue: Float {
        get {
            guard let acceptableValue = self.text as? Float else {
                fatalError("The NumericTextField assumes that is always has a text value that can be converted to a Float.")
            }
                
            return acceptableValue
        }
        set { self.text = "\(newValue)" }
    }
}

Using Swift’s fatalError lets us supply a crash message, which is much clearer than enforcing this behaviour via a force-unwrap:

class NumericTextField: UITextField {

    var floatValue: Float {
        get {
            // BAD!!  Don't use ! (force-unwrap)
            return self.text as! Float
        }
        set { self.text = "\(newValue)" }
    }
}

Solution #2: Prevent usage of the superclass’s API

The second thing we’ll prevent is usage of the UITextField.text property.

We can do this by overriding it and crashing.

class NumericTextField: UITextField {
    
    override var text: String? {
        set { fatalError("Use `floatValue` instead.") }
        get { fatalError("Use `floatValue` instead.") }
    }

    var floatValue: Float {
        get {
            guard let acceptableValue = super.text as? Float else {
                fatalError("Our error message...")
            }
                
            return acceptableValue
        }
        set { super.text = "\(newValue)" }
    }
}

Notice that we’ve also changed the floatValue getter and setter to use the .text property from the super class rather than self, since calling self.text will now crash.

We’ve now made it clear to the developer (or our future self) what the allowable usages of our class are.

Crashing in development is better than undefined behaviour in production.

With these two changes, we’ve made is easier for developers to use our API correctly.

This isn’t something you want to do on all of your APIs. You don’t want to allow anything external (such as user input or server responses) to be able to crash your app.

But the case above shows how you can use crashing as a tool to make sure that your code is using your other code correctly.

Please follow and like us:

Previous

11 Habits of an Effective Developer

Next

Using a Swift PropertyWrapper to ensure a closure is only called once

1 Comment

  1. Tomás

    This is the raison d’etre of typing systems. If the inputs in the typing system were strings they would need to be casted to floats, in most languages the developer would receive a compie time error (which is better than crashing) until they cast the input to a float and catch the plausible exception, or define a callback function for errors.

Leave a Reply

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

Powered by WordPress & Theme by Anders Norén