Search code examples
swiftmacosswiftuierror-handling

Swift custom LocalizedError lose failureReason in caller CatchBlock


trying first time to play around with error handling and error concepts. So I found this approach to create Error enums. Conforming to LocalizedError protocol I expected to have the failureReason as further explanation. Following is the enum for doing this.

enum NetworkError : LocalizedError {
  case invalidInput(String)
  case unexpected
  case invalidURL(_ url: URL)
  case apiError(statusCode: Int)
  
  var faliureReason: String? {
    switch self {
    case .invalidInput:
      return "Request body is nil."
    case .unexpected:
      return "..."
    case .invalidURL(_):
      return "..."
    case .apiError(statusCode: let statusCode):
      return "..."
    }
  }
}

in my caller function I've an catch block for this but the error.failureReason property remains nil.

func updateItem() async {
    guard let car = car else { return }
    
    let resource = PostCarResource(car: car)
    let request = APIRequest(resource: resource)
    
    var errorMessage = nil
    
    do {
      try await request.execute()
    } catch let error as LocalizedError {
      self.errorMessage = error.localizedDescription
      print(".")
      print("self.errorMessage")
      print(self.errorMessage)
      print(".")
      print("error.failureReason")
      print(error.failureReason ?? "no Reason")
      print(".")
      print("error")
      print(error)
      print(".")
      print("dump")
      dump(error)
    } catch {
      dump(error)
    }
  }

I'd expect that after the error has been set correctly to .invalidInput (what happens correctly) I could use error.failureReason but this is not the case. So why I'm not getting the String "Request body is nil."

The output of my console looks like:

.
self.errorMessage
Optional("The operation couldn’t be completed. (CarUSell.NetworkError error 0.)")
.
error.failureReason
no Reason
.
error
invalidInput("Request body is nil.")
.
dump
▿ -.NetworkError.invalidInput
  - invalidInput: "Request body is nil."

Solution

  • Looks like a simple typo. Your code defines

    var faliureReason
    

    That is not the same as failureReason. That's all, really.


    However, although this is not quite what you asked, note that you are using LocalizedError incorrectly. You do not need to catch a LocalizedError in a special way; it is an Error. LocalizedError is merely a way of constructing the error. So instead of

    } catch let error as LocalizedError {
    

    You should should just be saying

    } catch {
    

    The error will arrive as an Error called error, so now just read that. If you want to check whether it has a failure reason, cast to an NSError and check the localizedFailureReason:

    } catch {
        print((error as NSError).localizedFailureReason)
    }
    

    Here is a complete playground example:

    enum NetworkError : LocalizedError {
        case invalidInput(String)
        case unexpected
        case invalidURL(_ url: URL)
        case apiError(statusCode: Int)
    
        var failureReason: String? {
            switch self {
            case .invalidInput:
                return "Request body is nil."
            case .unexpected:
                return "..."
            case .invalidURL(_):
                return "..."
            case .apiError(statusCode: _):
                return "..."
            }
        }
    }
    
    func test() {
        do {
            throw NetworkError.invalidInput("what")
        } catch {
            print((error as NSError).localizedFailureReason) // "Request body is nil"
        }
    }
    
    test()
    

    Observe too that you have defined invalidInput(String) but you are not using the String in your failureReason. So I didn't use it either (but I had to supply a dummy string, when throwing, in order to get the example to compile). You probably meant something like this (I've marked the key changes with comments):

    enum MyNetworkError : LocalizedError {
        case invalidInput(String)
        case unexpected
        case invalidURL(_ url: URL)
        case apiError(statusCode: Int)
    
        var failureReason: String? {
            switch self {
            case .invalidInput(let message): // <--
                return message // <--
            case .unexpected:
                return "..."
            case .invalidURL(_):
                return "..."
            case .apiError(statusCode: _):
                return "..."
            }
        }
    }
    
    func test() {
        do {
            throw MyNetworkError.invalidInput("Request body is nil") // <--
        } catch {
            print((error as NSError).localizedFailureReason) // "Request body is nil"
        }
    }
    
    test()