Type of Swift error type

Simple domain error

Simple, obvious mistakes. The best thing about this type of error is that we don’t need to know why it happened, just that we know it happened and want to fix it. The way to indicate that this error has occurred is to return a nil value. In Swift, the most common cases of this kind of error are converting a string to an integer, or trying to fetch an element with a nonexistent key in a dictionary:

Let num = Int("hello world") // nil let element = dic["key_not_exist"] // nilCopy the code

At the usage level (or application logic), this type of error is usually handled in advance by the optional binding of the IF let or by the Guard let, without additional processing at the language level.

Recoverable error

As the name suggests, such errors should be tolerated and recoverable. Recoverable errors occur as part of a normal program path, and it is up to us as developers to detect such errors and further process them to restore them to the desired program path.

This type of Error was usually represented in objective-C times as an NSError type, whereas in Swift it is a combination of throw and Error. Generally we need to check for the type of error and respond appropriately. It is often unwise to ignore such errors, as they are likely to occur during normal use and should be attempted to recover or at least give the user a reasonable indication of what is going on. Such as network request timeout, or write file disk space is insufficient:

// Let url = url (string: "https://www.example.com/")! let task = URLSession.shared.dataTask(with: Url) {data, response, error in if let error = error { (error.localizedDescription)") } let data = data! / /... } // write the file func write(data: data, to url: url) {do {try data.write(to: Url)} to catch the let the error as NSError {. If the error code = = NSFileWriteOutOfSpaceError {/ / automatic recovery attempt by releasing space removeUnusedFiles () Write (data: data, to: url)} else {showErrorAlert("Error: (error.localizedDescription)") } } catch { showErrorAlert("Error: (error.localizedDescription)") } }Copy the code

Universal error

Such errors are theoretically recoverable, but due to the nature of the language, it is difficult to know the source of such errors, so they are generally not handled. This type of error includes situations like the following:

// Run out of memory [Int](repeating: 100, count:.max) // Call stack overflow func foo() {foo()} foo()Copy the code

We can design ways to handle these errors, such as detecting the current memory footprint and warning if it exceeds a certain value, or monitoring the stack frame limit. But in general this is not necessary, nor is it possible to cover all error cases. More often than not, this is the result of code bumping into physical limitations and boundary conditions on the device, which we generally don’t deal with (unless it’s an artificial bug).

In Swift, all kinds of errors forcibly terminated by fatalError can generally be classified as Universal error.

Logic failure

Logic errors are errors caused by programmer errors that should be fixed and avoided completely at development time through code, not recovered and handled at run time.

Common Logic failures include:

// Force unpack a 'nil' optional var name: String? = nil name! Let arr = [1,2,3] let num = arr[3] var a = int. Max a += 1 JSONDecoder().decode(Foo.self, from: Data())Copy the code

Such errors typically trigger both assert and precondition in the implementation.

Scope of assertions and error conversion

Unlike fatalError, Assert is not triggered only in the -O configuration for compilation optimization, and precondition is not triggered if you further configure the compilation optimization option to -ounchecked. At this point, the precondition in each method is skipped so that we get the fastest running speed. But the code is also relatively less secure because we get indeterminate behavior for errors such as out-of-bounds access or computation overflows.

function faltaError precondition assert
-Onone The trigger The trigger The trigger
-O The trigger The trigger
-Ounchecked The trigger

You typically use fatalError for Universal Error, and Assert or Precondition for Logic Failure. Following this rule will help define errors when coding. Sometimes we want to capture as many Logic failures as possible during development and minimize crashes after release. In this case, it is better to use Assert to check internally to crash the program at development time than to convert Logic failure directly into a recoverable error.

Quiz

All talk, no practice. Let’s actually decide which error handling method to use in each of these situations

1. Load resources within the app

Please listen to.

Suppose we are working with a machine learning model and need to read a pre-trained model from disk. The model is stored as a file in the app bundle. What should we do if the model is not found when we read it?

Solution 1 Simple Domain Error

func loadModel() -> Model? {
    guard let path = Bundle.main.path(forResource: "my_pre_trained_model", ofType: "mdl") else {
        return nil
    }
    let url = URL(fileURLWithPath: path)
    guard let data = try? Data(contentOf: url) else {
        return nil
    }
    
    return try? ModelLoader.load(from: data)
}
Copy the code

Solution 2 Recoverable Error

func loadModel() throws -> Model {
    guard let path = Bundle.main.path(forResource: "my_pre_trained_model", ofType: "mdl") else {
        throw AppError.FileNotExisting
    }
    let url = URL(fileURLWithPath: path)
    let data = try Data(contentOf: url)
    return try ModelLoader.load(from: data)
}
Copy the code

Scheme 3 Universal Error

func loadModel() -> Model {
    guard let path = Bundle.main.path(forResource: "my_pre_trained_model", ofType: "mdl") else {
        fatalError("Model file not existing")
    }
    let url = URL(fileURLWithPath: path)
    do {
        let data = try Data(contentOf: url)
        return try ModelLoader.load(from: data)
    } catch {
        fatalError("Model corrupted.")
    }
}
Copy the code

Solution 4 Logic Failure

func loadModel() -> Model {
    let path = Bundle.main.path(forResource: "my_pre_trained_model", ofType: "mdl")!
    let url = URL(fileURLWithPath: path)
    let data = try! Data(contentOf: url)
    return try! ModelLoader.load(from: data)
}
Copy the code

The correct answer is option 4, which uses Logic Failure to crash the code directly.

If a model or configuration file that is built into the app bundle does not exist or cannot be initialized, without regard to extreme factors, there must be a development problem. This should not be a recoverable error, and the result will be the same no matter how many retries. Maybe the developer forgot to put the file in the right place, or maybe there’s something wrong with the file itself. In either case, we want to catch the bugs early and force us to fix them, and letting the code crash does this well.

Code can also crash with a Universal error, but a Universal error is more commonly used in language-bound cases. That’s not the case here.

2. An error occurred when loading the current user information

We will store the user information locally after the user logs in, and we will detect and use the user information every time we reopen the APP. If user information does not exist, the following operations should be performed:

Solution 1 Simple Domain Error

func loadUser() -> User? {
    let username = UserDefaults.standard.string(forKey: "com.onevcat.app.defaults.username")
    if let username {
        return User(name: username)
    } else {
        return nil
    }
}
Copy the code

Solution 2 Recoverable Error

func loadUser() throws -> User {
    let username = UserDefaults.standard.string(forKey: "com.onevcat.app.defaults.username")
    if let username {
        return User(name: username)
    } else {
        throws AppError.UsernameNotExisting
    }
}
Copy the code

Scheme 3 Universal Error

func loadUser() -> User {
    let username = UserDefaults.standard.string(forKey: "com.onevcat.app.defaults.username")
    if let username {
        return User(name: username)
    } else {
        fatalError("User name not existing")
    }
}
Copy the code

Solution 4 Logic Failure

func loadUser() -> User {
    let username = UserDefaults.standard.string(forKey: "com.onevcat.app.defaults.username")
    return User(name: username!)
}
Copy the code

Let’s definitely rule out options 3 and 4. The user name does not exist. So we should choose between plan 1 and Plan 2.

In this case, option 1 Simple Domain Error is better. Because it’s a simple case where the user doesn’t exist, if the user doesn’t exist, then we just let the user log in, and we don’t need to know any extra error information, and returning nil is a good way of expressing intent.

Of course, we do not rule out that as the situation becomes more complex in the future, we will need to distinguish the reasons for the loss of user information (such as whether the new user has not registered, or the original user has logged out). However, in the current situation, this belongs to excessive design and does not need to be considered for the time being. If the business gets this complicated later, it is not too difficult to change the Simple Domain error to Recoverable error with the help of the compiler.

3. Code that has not been implemented

Let’s say you’re developing an iOS framework for your service, but due to a limited schedule, some features are only defined as interfaces, not implemented. These interfaces will be available in the official release, but we will need to pre-release them for private testing. So instead of explicitly documenting these things, what happens inside these methods?

Solution 1 Simple Domain Error

func foo() -> Bar? {
    return nil
}
Copy the code

Solution 2 Recoverable Error

func foo() throws -> Bar? {
    throw FrameworkError.NotImplemented
}
Copy the code

Scheme 3 Universal Error

func foo() -> Bar? {
    fatalError("Not implemented yet.")
}
Copy the code

Solution 4 Logic Failure

func foo() -> Bar? {
    assertionFailure("Not implemented yet.")
    return nil
}
Copy the code

The correct answer is plan 3 Universal error. For unimplemented methods, it makes no sense to return nil or throw an error expecting the user to recover, further confusing framework users. The problem here is the boundary case at the language level, and since it is not implemented, we need to give a strong reminder. In any build setting, users should not be expected to successfully call this function, so fatalError is the best choice.

4. Invoke the sensor on the device to collect data

The app that calls the sensor is the most interesting! Whether it’s a camera or a gyroscope, sensor-related apps are a lot of fun. So what if an error occurs when you want to call a sensor to get data?

Solution 1 Simple Domain Error

func getDataFromSensor() -> Data? {
    let sensorState = sensor.getState()
    guard sensorState == .normal else {
        return nil
    }
    return try? sensor.getData()
}
Copy the code

Solution 2 Recoverable Error

func getDataFromSensor() throws -> Data { let sensorState = sensor.getState() guard sensorState == .normal else { throws  SensorError.stateError } return try sensor.getData() }Copy the code

Scheme 3 Universal Error

func loadUser() -> Data { let sensorState = sensor.getState() guard sensorState == .normal, let data = try? sensor.getData() else { fatalError("Sensor get data failed!" ) } return data }Copy the code

Solution 4 Logic Failure

func loadUser() -> Data {
    let sensorState = sensor.getState()
    assert(sensorState == .normal, "The sensor state is not normal")
    return try! sensor.getData()
}
Copy the code

It is quite possible that the sensor is temporarily unavailable for a variety of reasons (such as being occupied by another process, or even if the corresponding sensor does not exist on the device). Even if the sensor data is critical and indispensable to the application, we might want to at least give the user a hint. Based on this consideration, option 2 Recoverable Error is a reasonable choice.

Option 1 May also be a simpler option when sensor data is inconsequential. But options 3 and 4 will crash the program directly, and this is not actually a code boundary or developer error, so should not be considered.

Some summary of Quiz

It can be seen that in error handling, what kind of error is chosen depends on the situation and processing requirements. I also used a lot of statements such as “possible” and “comparatively” in reference answers. While intuitive considerations and decisions can be made for a particular scenario, they are not dogmatic. Error types can be easily converted into each other through code, which gives us the freedom to choose the strategy to use when dealing with errors: for example, even though the API provides us with the throws form of Recoverable, we can still, as needed, pass a try? Change it to Simple Domain Error, or use a try! Change it to Logic Failure.

In order to truly understand the classification basis of these four kinds of errors, we can make use of these conversion methods and flexibly select the most appropriate error type in the application scenario.

Some summary of Quiz

It can be seen that in error handling, what kind of error is chosen depends on the situation and processing requirements. I also used a lot of statements such as “possible” and “comparatively” in reference answers. While intuitive considerations and decisions can be made for a particular scenario, they are not dogmatic. Error types can be easily converted into each other through code, which gives us the freedom to choose the strategy to use when dealing with errors: for example, even though the API provides us with the throws form of Recoverable, we can still, as needed, pass a try? Change it to Simple Domain Error, or use a try! Change it to Logic Failure.

In order to truly understand the classification basis of these four kinds of errors, we can make use of these conversion methods and flexibly select the most appropriate error type in the application scenario. The original | address

recommended

  • Error Handling in Swift 2.0
  • The very expressive nature reserch reserch ら espa espa expressing system ほ arashii
  • IOS development technical materials