Select Page

A few months ago, I stumbled upon a discussion in the thoughtbot guides about the use of fatal errors in Swift. It seems every developer has an opinion about fatal errors and a consensus hasn’t been reached yet in the Swift community. The only mention in The Swift Programming Language is in the section that discusses the guard statement and early exit.

When I fist encountered the fatalError(_:file:line:) function, it reminded me of the abort() function. Even though both functions cause the immediate termination of your application, the fatalError(_:file:line:) function is different and, in the context of Swift, it is much more useful.

What to Do When You Don’t Know What to Do

Ever since I started working with Swift, I have been struggling with the implementation of the tableView(_:cellForRowAt:) method. If you think that sounds silly, then take a look at the following example.

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    if let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.reuseIdentifier, for: indexPath) as? SettingsTableViewCell {
        // Configure Cell
        cell.textLabel?.text = "Some Setting"

        return cell

    } else {
        return UITableViewCell()
    }
}

There are several variations of the above implementation and I have tried all of them. The idea is simple. We expect an instance of the SettingsTableViewCell class if we ask the table view for a cell with the reuse identifier of the SettingsTableViewCell class. Because the dequeueReusableCell(withIdentifier:for:) method returns a UITableViewCell instance, we need to cast the result to an instance of the SettingsTableViewCell class.

This is inconvenient since we always expect to receive a SettingsTableViewCell instance if we ask the table view for a cell with the reuse identifier of the SettingsTableViewCell class. We could use as! instead of as?, but that is not a solution I feel comfortable with. I avoid the exclamation mark whenever I can.

If, for some reason, something goes wrong, we return a UITableViewCell instance from the tableView(_:cellForRowAt:) method. But that should never happen. Right?

While this is fine and necessary to make sure we return a UITableViewCell instance from the tableView(_:cellForRowAt:) method, I hope you can see that we are implementing a workaround for a scenario we do not expect, a scenario that should never occur.

Guard Against Unexpected Events

Every time I implement tableView(_:cellForRowAt:) I wonder if there is a better approach. And there is. We need to guard against the event that the table view hands us a UITableViewCell instance we don’t expect and, if that happens, we throw a fatal error.

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.reuseIdentifier, for: indexPath) as? SettingsTableViewCell else { fatalError("Unexpected Index Path") }

    // Configure Cell
    ...

    return cell
}

The application crashes if a fatal error is thrown. Why is this better? This is a better solution for two reasons.

Unexpected State

If the application runs into a scenario in which a fatal error is thrown, we communicate to the runtime that the application is in a state it does not know how to handle.

In the first example, the solution was to return a UITableViewCell instance. Does that solve the problem? No. The application ignores the situation and avoids causing havoc by returning a UITableViewCell instance. The application avoids dealing with the unexpected state it is in.

Finding and Fixing the Bug

If the application runs into a state we did not anticipate, it means a bug has slipped into the codebase. If the application is terminated due to a fatal error being thrown, we have work to do. It means we need to find the bug and fix it.

The above solution, using a guard statement and throwing a fatal error, is a solution I am very happy with. It avoids the obsolete if statement of the first implementation of the tableView(_:cellForRowAt:) method and it correctly handles the situation. The result is a much more elegant implementation of the tableView(_:cellForRowAt:) method. And it adds clarity to the implementation of the tableView(_:cellForRowAt:) method.

Use Fatal Errors Sparingly

This doesn’t mean that you need to use fatal errors whenever you want to avoid error handling or the application enters a state that is hard to recover from. I use fatal errors only when the application can enter in a state it was not designed for. Take a look at the following example.

import Foundation

enum Section: Int {

    case news
    case profile
    case settings

    var title: String {
        switch self {
        case .news: return NSLocalizedString("section_news", comment: "news")
        case .profile: return NSLocalizedString("section_profile", comment: "profile")
        case .settings: return NSLocalizedString("section_settings", comment: "settings")
        }
    }

}

struct SettingsViewViewModel {

    func title(for section: Int) -> String {
        guard let section = Section(rawValue: section) else { fatalError("Unexpected Section") }
        return section.title
    }

}

The title(for:) method of the SettingsViewViewModel struct does not expect a value it cannot use to instantiate a valid Section instance with. If the value of the section parameter is invalid, it would not know what to do. In that scenario, a fatal error is thrown.

If the application does enter that scenario, it means you made a logical mistake and it is your task to find out why and how it can be resolved.

Clarity and Elegance

The use of the fatalError(_:file:line:) function has made my code more readable without compromising the inherent safety of the Swift language. It adds clarity to the code I write and Swift regains its elegance. Give it a try and let me know if you like it.

Share This