Chris Amanse

A blog on software engineering, and iOS

Catching Multiple Errors in Swift

Currently, I’m building a one-time password generator app, and for the part in validating user input, I’m using Swift’s error-handling statements to catch errors in the input. In order to write less code, I tried catching multiple errors in a catch statement:

do {
  try validate(input)
} catch InvalidInput.noAccount, InvalidInput.noKey {
  // No account and no key
}

As it turns out, it’s not yet possible in Swift. Therefore, I tried to find “ways” to catch multiple errors in Swift.

First Solution: Recursive Enum

My first attempt to solve this is to add a case in the enum that has an associated value of an array of cases:

enum InvalidInput: Error {
  case noAccount
  case noKey
  indirect case errors([InvalidInput])
}

Now, for the catch statement, all I have to do is catch the InvalidInput.errors case:

do {
  try validate(input)
} catch InvalidInput.errors(let errors) where errors == [.noAccount, .noKey] {
    // Handle no account, and no key case
}

That’s great! However, since errors is an array, the order matters when comparing with another array. Thus, if errors is equal to [.noKey, .noAccount], the error will not be caught by the catch statement above. To fix this, we can use a Set instead of an Array. But, we still have to conform the InvalidInput enum to the Hashable protocol. That seems to complicate our code more.

Second Solution: OptionSet

My first attempt was not really a pretty solution. The reason is that I still tried to use an enum. The beauty of Swift’s error handling is that we can throw any type that conforms to the Error protocol. That means, we can use a struct instead that conforms to OptionSet.

struct InvalidInput: OptionSet, Error {
  public var rawValue: UInt8
  
  public init(rawValue: UInt8) {
    self.rawValue = rawValue
  }
  
  static let noAccount = InvalidInput(rawValue: 1 << 0)
  static let noKey     = InvalidInput(rawValue: 1 << 1)
}

We can now simply throw an InvalidInput type instead of creating an array of errors:

func validate(_ input: Input) throws -> Void {
  var errors: InvalidInput = []
  
  if !input.hasAccount {
    errors.insert(.noAccount)
  }
  
  if !input.hasKey {
    errors.insert(.noKey)
  }
  
  guard errors.isEmpty else {
    throw errors
  }
  
  print("No errors found")
}

For the catch statement, we can catch the error variable instead:

do {
  try validate(input)
} catch let error as InvalidInput {
  // Handle error
}

Error messages

We can even extend the InvalidInput type to give it’s error messages:

extension InvalidInput {
  var errorMessages: [String] {
    let messages = [String]()
    
    if self.contains(.noAccount) {
      messages.append("Account is required.")
    }
    
    if self.contains(.noKey) {
      messages.append("Key is required.")
    }
    
    return messages
  }
}

Finally, here’s our catch statement:

do {
  try validate(input)
} catch let error as InvalidInput {
  let message = error.errorMessages.joined(separator: " ")
  showAlert(title: "Found Errors", message: message)
}

If we have more options, we can even have catch statements that catches specific options only:

do {
  try validate(input)
} catch [.invalidAccount, .invalidKey] as AddAccountInvalidInput {
  // Handle invalid inputs
} catch [.accountExists, .noKey] as AddAccountInvalidInput {
  // Account exists, and no key
} catch .noAccount {
  // No account
}

That’s it! By using an OptionSet instead, we can have more flexible catch statements. This is especially useful for showing errors in a form. Instead of having one error message, we can inspect if the thrown error contains specific errors, and display that error in the corresponding input field.

Here’s the final code:

struct InvalidInput: OptionSet, Error {
  public var rawValue: UInt8
  
  public init(rawValue: UInt8) {
    self.rawValue = rawValue
  }
  
  static let noAccount = InvalidInput(rawValue: 1 << 0)
  static let noKey     = InvalidInput(rawValue: 1 << 1)
  
  var errorMessages: [String] {
    let messages = [String]()
    
    if self.contains(.noAccount) {
      messages.append("Account is required.")
    }
    
    if self.contains(.noKey) {
      messages.append("Key is required.")
    }
    
    return messages
  }
}
Newer >>