Fatal errors have a negative connotation and with reason. You should use them sparingly if you want to avoid having your application crash and burn at the slightest hiccup. Despite their negative undertone, fatal errors are an integral part of my workflow as I write elsewhere in this book.
Whenever I write or speak about my use of fatal errors, I usually see two types of responses. Developers unfamiliar with fatal errors and how they can be used safely are surprised and excited. They spot the benefits fatal errors can bring to a project. Can you guess what the second type of response sounds like?
Why don't you use an exclamation mark instead?
The suggestion to use an exclamation mark instead of throwing a fatal error is understandable. From a user's perspective, the result is identical. But I'm not using fatal errors with the user in mind. I don't throw a fatal error to crash the application when the user is using it. In an ideal scenario, a fatal error should only be thrown in development or when the application is being tested.
I agree that the user won't appreciate my use of fatal errors if the application crashes the moment they're about to best their previous high score. The thing is that I'm a developer and I look at code most of my working hours. And that's exactly the reason I choose for fatal errors more frequently than I choose for the exclamation mark. Let me explain what I mean by that.
Clarity Over Subtleness
My biggest complaint with the exclamation mark is its subtleness. Ironically, plenty of developers use the exclamation mark for exactly that reason. It's so easy to append an exclamation mark to a variable or a constant. It's almost too easy. I understand why the Swift team has chosen to support forced unwrapping and forced conversion using the as!
operator, but I wouldn't shed a tear if both were removed from the Swift language.
I agree that it can be frustrating to interact with an ancient Objective-C API that doesn't care about nil
and optionals. Interacting with the file system, for example, can often lead you down a rabbit hole of optionals, indentation, and conditionals. But that's what it takes if you decide to write software in Swift.
Don't be lazy by appending an exclamation mark to a variable or a constant you're pretty sure will always contain a value. It will contain a value ... most of the time ... almost always. As the documentation explains, you should only force unwrap an optional if you're absolutely certain that it contains a value. I turn it around when I use fatal errors. A fatal error should be thrown if the application enters a state it didn't anticipate.
Choosing for Clarity
A common trait among developers is an obsession with simplicity and minimalism. Clean code is but one manifestation of this trait. By using fatal errors I choose for clarity. If the application throws a fatal error, I want to know about it. It's true that the exclamation mark will also do that for me. But I also want to know about it when I'm simply reading through my code.
An exclamation mark doesn't jump out, but a guard
statement with a fatalError()
call does. It immediately shows you that you know that a certain scenario should never happen and you guard against that.
Take a look at the following implementation of the prepare(for:sender:)
method. This is a common pattern I use. If the user triggers the Segue.SelectProfile
segue, the application expects the destination view controller to be of type SelectProfileViewController
. It simply doesn't know how to respond if that isn't true hence the fatal error in the else
clause of the guard
statement. While it may look a bit verbose, it's clear and explicit.
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let identifier = segue.identifier else { return }
switch identifier {
case Segue.SelectProfile:
guard let destination = segue.destination as? SelectProfileViewController else {
fatalError()
}
...
default: break
}
}
The alternative is to use the as!
operator to forcefully convert the destination view controller to the type the application expects.
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let identifier = segue.identifier else { return }
switch identifier {
case Segue.SelectProfile:
let destination = segue.destination as! SelectProfileViewController
...
default: break
}
}
I understand that this practice is a bit controversial, but I have seen its effectiveness. It's why I'm a big fan of this subtle but elegant practice. Give it a try and let me know what you think.