Error handling

According to the authors of The Swift Progression, a good error-handling architecture should have the following characteristics:

  • Conciseness: Logically correct code should not be drowned out by code that throws errors.
  • Propagation: Errors do not have to be handled in the same place. In general, it is better to keep error-handling code away from where errors occur.
  • Documentation: Allows programmers to determine where errors are likely to occur and the specific types of errors.
  • Safety: Helps programmers ignore error handling for unexpected reasons.
  • Universality: Develop a mechanism for throwing and handling errors that can be used in all scenarios.

Wrong type

  • Trivial Errors: Some operations have only one predictable failure.
  • Rich Errors requiring detailed information: For network and file system operations, they should provide more substantive problem descriptions about failures than a simple “something works.”
  • Unexpected errors: These are errors that result from conditions that the programmer did not anticipate. The way to deal with such errors is usually to crash the program.

In our code, we use various types of assertions (such as Assert, Precondition, or fatalError) to identify the desired result and to interrupt the program if conditions are not met.

The Result type

enum Result<Success.Failure: Error> {
  case success(Success)
  case failure(Failure)}Copy the code

To write a function that reads a file from disk.

enum FileError: Error {
  case fileDoesNotExit
  case noPermission
}
Copy the code
func contents(ofFile filename: String) -> Result<String.FileError>
Copy the code
let result = contents(ofFile: "input.txt")
switch result {
case let .success(contents):
  print(contents)
case let .failure(error):
  switch error {
    case .fileDoesNotExist:
    	print("File not found")
    case .noPermission:
    	print("No permission")}}Copy the code

Throw and catch

For each function call that can throw an error, the compiler verifies that the caller caught the error or passes the error up to its caller. Throw contents(ofFile:) with throws syntax:

func contents(ofFile filename: String) throw -> String
Copy the code
do {
  let result = try contents(ofFile: "input.txt")
  print(result)
} catch FileError.fileDoesNotExist {
  print("File not found")}catch {
  print(error)
  // Handle other errors.
}
Copy the code

Specific type error and no type error

  • Swift’s native error mechanism uses untyped errors. We can only use throws to declare that a function throws errors, but cannot specify which specific errors it throws.

  • Result: Typed errors. Result takes two generic parameters, Success and Failure, and the latter specifies the specific type of error.

  • Specific types of errors have their own serious problems:

    • The specific error types make it difficult to combine functions that throw exceptions, and to aggregate the errors thrown by those functions.
    • Strict error types limit the extensibility of libraries.
    • Unlike having to walk through all the members of an enumeration, it is often unnecessary and unrealistic to expect to handle all error cases.

    Because errors are untyped, it is important to document any errors that a function might throw. Xcode supports the use of the Throws keyword to mark errors thrown by functions in code comments. Here’s an example:

    /// Opens a text file and returns its contents.
    ///
    /// - Parameter filename: The name of the file to read.
    /// - Returns: The file contents, interpreted as UTF-8.
    /// - Throws: `FileError` if the file does not exist or
    /// the process doesn't have read permissions.
    func contents(ofFile filename: String) throws -> String
    Copy the code

    This way, when you hold down Option and click on the function name, the quick help panel that pops up has a section dedicated to displaying errors.

A non-negligible error

For example, consider the data.write (to:options:) method in Foundation (writing several bytes to a file) or the Filemanager.removeItem (at:) method (deleting a specified file) :

extension Data {
  func write(to url:URL.options: Data.WritingOptions = []) throws
}

extension FileManager {
  func removeItem(at URL: URL) throws
}
Copy the code

If these methods use result-based error handling, their declarations might look something like this:

extension Data {
  func write(to url: URL.options: Data.WritingOptions = []) -> Result< (),Error>}extension FileManager {
  func removeItem(at URL: URL) -> Result< (),Error>}Copy the code

What makes these methods special is that we call them for their side effects, not for the return value. In fact, neither method has a truly meaningful return value, except after indicating whether the operation succeeded. Therefore, the above two versions of Result, intentional or not, are too easy for programmers to ignore any failures and probably write code like this:

_ = FileManager.default.removeItem(at:url)
Copy the code

Throws version, the compiler forces us to use the try prefix before the call. The compiler also requires us to either nest the call in a do/catch block or pass the error up the call stack. This is a clear and clear reminder, both to the programmer who wrote this code and to the reader of this code, that the currently called function is likely to fail and that the compiler will force us to deal with errors.

Error converting

Throws between throws and Optionals

Errors and optional values are common ways for a function to report that something is not working correctly.

try? The keyword allows us to ignore errors thrown by the function and make the return value of the function Optional of the original return value. This Optional option tells us if the function executed successfully:

if let result = try? parse(text: input) {
  print(result)
}
Copy the code

Similarly, in order to turn a function that returns Optional into a function that throws an error, we have to supply nil with the corresponding error value.

extension Optional {
  /// Unpack self if it is not nil.
  /// If 'self' is' nil ', an error is thrown.
  func or(error:Error) throws -> Wrapped {
    switch self {
      case let x?: return x
      case nil: throw error
    }
  }
}
Copy the code
do {
  let int = try Int("42").or(error: ReadIntError.couldNotRead)
} catch {
  print(error)
}
Copy the code

There is a third form of try: try! . Use this form only if you are sure that the function is absolutely impossible to fail. Similar to forcing Optional unpacking, if you use try! The called function throws an error, causing the App to blink.

Converts between throws and Result

Result and throws keywords are only two different presentation modes of Swift error handling mechanism.

To call a function that can throw an error and wrap its return value as a Result, you can use the init(catching:) initialization method, which takes a function that can throw an error as an argument and wraps its return value as a Result object.

extension Result where Failure= =Swift.Error {
  /// Create a new 'Result' object by evaluating the return value of an error-throwing function,
  // wrap the return result on success in 'case success' and throw an error on failure
  /// wrap in 'case failure'.
  init(catching body: () throws -> Success) {
    do {
      self = .success(try body())
    } catch {
      self = .failure(error)
    }
  }
}
Copy the code
let encoder = JSONEncoder(a)let encodingResult = Result { try encoder.encode([1.2])}// success(5 bytes)
type(of: encodingResult) // Result<Data,Error>
Copy the code

The opposite of init(catching:) is called result.get (). It evaluates the Result of Result and throws the value from Failure as an error.

extension Result {
  public func get(a) throws -> Success {
    switch self {
      case let .success(success):
      	return success
      case let .failure(failure):
      	throws failure
    }
  }
}
Copy the code

The error chain

It is very common to call multiple functions in succession that might throw an error.

Throws the chain

func complexOperation(filename: String) throws- > [String] {
  let text = try contents(ofFile: filename)
  let segments = try parse(text: text)
  return try process(segments: segments)
}
Copy the code

Result the chain

func complexOperation2(filename: String) -> Result"[String].Error> {
  return contents(ofFile: filename).mapError { $0 as Error }
  	.flatMap { text in parse(text: text).mapError { $0 as Error } }
  	.flatMap { segments in
             process(segments: Segments).mapError { $0 as Error}}}Copy the code

The job of mapError is to generalize a specific Error type to “a type that implements Error.”

Errors in asynchronous code

func computeResult(callback: (Result<Int.Error>) - > ())
Copy the code

See, a throw is a lot like a return in that it can only work in one direction, namely passing messages up. We can pass the error to the caller of a function, but we cannot pass the error down as an argument to other functions that will be called later.

Nowadays, we can only stick with Result in asynchronous code where errors occur.

Use defer to clean up

  • When you leave the current scope, the code block that defer specified will always be executed, whether because the execution returned successfully, or because some error occurred, or for some other reason. This makes thedeferCode blocks become the preferred place to perform cleanup.
  • If there are more than one scopedeferBlocks of code, which will be in the order definedThe reverseThe execution.
  • adeferThe code block will leave in the programdeferWhen the scope defined is executed. evenreturnStatements are evaluated in the same scopedeferComplete before being executed. You can use this feature to change the value of a variable after returning it.
  • Of course, there are somedeferA condition in which a statement will not execute, such as when a program has a segment error, or when a fatal error is triggeredfatalErrorFunction or force unpacknil), all code execution is immediately terminated.

Rethrows

Marking a function with REthrows tells the compiler that the function will throw an error only if its arguments throw an error. So. The final signature of the filter method looks like this:

func filter(_ isIncluded: (Element) throws -> Bool) rethrows- > [Element]
Copy the code

The predicate function is still marked as a throws function, indicating that the caller may pass a function that throws an error.

In the standard library, methods that take function type parameters are labeled with REthrows for almost all sequence and collection types. One exception is lazy-loaded collection methods.

Bridge the error to Objective-C

For example, the Objective-C version of contents(ofFile:) might be written like this:

- (NSString *)contentsOfFile(NSString *)filename error: (NSError 天安门事件)error;
Copy the code

Swift automatically converts a method that takes NSError ** as an argument to a version of its throws syntax. When it is imported into Swift, it looks like this:

func contents(ofFile filename: String) throws -> String
Copy the code

If you pass a Swift error to an objective-c method, similarly, it will be bridged to an NSError.