Handling errors in Swift Handling errors in Swift swift swift

Handling errors in Swift


First of all this is a great question. Error handling is a specific task that applies to a incredible array of situations with who know's what repercussions with your App's state. The key issue is what is meaningful to your user, app and you the developer.

I like to see this conceptually as how the Responder chain is used to handle events. Like an event traversing the responder chain an error has the possibility of bubbling up your App's levels of abstraction. Depending on the error you might want to do a number of things related to the type of the error. Different components of your app may need to know about error, it maybe an error that depending on the state of the app requires no action.

You as the developer ultimately know where errors effect your app and how. So given that how do we choose to implement a technical solution.

I would suggest using Enumerations and Closures as to build my error handling solution.

Here's a contrived example of an ENUM. As you can see it is represents the core of the error handling solution.

    public enum MyAppErrorCode {    case NotStartedCode(Int, String)    case ResponseOkCode    case ServiceInProgressCode(Int, String)    case ServiceCancelledCode(Int, String,  NSError)    func handleCode(errorCode: MyAppErrorCode) {        switch(errorCode) {        case NotStartedCode(let code, let message):            print("code: \(code)")            print("message: \(message)")        case ResponseOkCode:            break        case ServiceInProgressCode(let code, let message):            print("code: \(code)")            print("message: \(message)")        case ServiceCancelledCode(let code, let message, let error):            print("code: \(code)")            print("message: \(message)")            print("error: \(error.localizedDescription)")        }    }}

Next we want to define our completionHandler which will replace ((error: NSError?) -> Void) the closure you have in your download method.

((errorCode: MyAppErrorCode) -> Void)

New Download Function

func download(destinationUrl: NSURL, completionHandler: ((errorCode: MyAppErrorCode) -> Void)) {    let request = NSURLRequest(URL: resourceUrl!)    let task = downloadSession.downloadTaskWithRequest(request) {        (url: NSURL?, response: NSURLResponse?, error: NSError?) in        if error == nil {            do {                try self.fileManager.moveItemAtURL(url!, toURL: destinationUrl)                completionHandler(errorCode: MyAppErrorCode.ResponseOkCode)            } catch let e {                print(e)                completionHandler(errorCode: MyAppErrorCode.MoveItemFailedCode(170, "Text you would like to display to the user..", e))            }        } else {            completionHandler(errorCode: MyAppErrorCode.DownloadFailedCode(404, "Text you would like to display to the user.."))        }    }.resume()}

In the closure you pass in you could call handleCode(errorCode: MyAppErrorCode) or any other function you have defined on the ENUM.

You have now the components to define your own error handling solution that is easy to tailor to your app and which you can use to map http codes and any other third party error/response codes to something meaningful in your app. You can also choose if it is useful to let the NSError bubble up.


EDIT

Back to our contrivances.

How do we deal with interacting with our view controllers? We can choose to have a centralized mechanism as we have now or we could handle it in the view controller and keep the scope local. For that we would move the logic from the ENUM to the view controller and target the very specific requirements of our view controller's task (downloading in this case), you could also move the ENUM to the view controller's scope. We achieve encapsulation, but will most lightly end up repeating our code elsewhere in the project. Either way your view controller is going to have to do something with the error/result code

An approach I prefer would be to give the view controller a chance to handle specific behavior in the completion handler, or/then pass it to our ENUM for more general behavior such as sending out a notification that the download had finished, updating app state or just throwing up a AlertViewController with a single action for 'OK'.

We do this by adding methods to our view controller that can be passed the MyAppErrorCode ENUM and any related variables (URL, Request...) and add any instance variables to keep track of our task, i.e. a different URL, or the number of attempts before we give up on trying to do the download.

Here is a possible method for handling the download at the view controller:

func didCompleteDownloadWithResult(resultCode: MyAppErrorCode, request: NSURLRequest, url: NSURL) {    switch(resultCode) {    case .ResponseOkCode:        // Made up method as an example        resultCode.postSuccessfulDownloadNotification(url, dictionary: ["request" : request])    case .FailedDownloadCode(let code, let message, let error):        if numberOfAttempts = maximumAttempts {            // Made up method as an example            finishedAttemptingDownload()        } else {             // Made up method as an example            AttemptDownload(numberOfAttempts)        }    default:        break    }}


Long story short: yes

... and then write a lot of code that differentiates the messages/action taken based on the error code?

Most code examples leave the programmer alone about how to do any error handling at all, but in order to do it right, your error handling code might be more than the code for successful responses. Especially when it comes to networking and json parsing.

In one of my last projects (a lot of stateful json server communication) I have implemented the following approach: I have asked myself: How should the app possibly react to the user in case of an error (and translate it to be more user friendly)?

  • ignore it
  • show a message/ an alert (possibly only one)
  • retry by itself (how often?)
  • force the user to start over
  • assume (i.e. a previously cached response)

To achieve this, I have create a central ErrorHandler class, which does have several enums for the different types of errors (i.e. enum NetworkResponseCode, ServerReturnCode, LocationStatusCode) and one enum for the different ErrorDomains:

enum MyErrorDomain : String {    // if request data has errors (i.e. json not valid)    case NetworkRequestDomain   = "NetworkRequest"    // if network response has error (i.e. offline or http status code != 200)    case NetworkResponseDomain  = "NetworkResponse"    // server return code in json: value of JSONxxx_JSON_PARAM_xxx_RETURN_CODE    case ServerReturnDomain = "ServerReturnCode"    // server return code in json: value of JSONxxxStatus_xxx_JSON_PARAM_xxx_STATUS_CODE    case ServerStatusDomain = "ServerStatus"    // if CLAuthorizationStatus    case LocationStatusDomain = "LocationStatus"    ....}

Furthermore there exists some helper functions named createError. These methods do some checking of the error condition (i.e. network errors are different if you are offline or if the server response !=200). They are shorter than you would expect.

And to put it all together there is a function which handles the error.

func handleError(error: NSError, msgType: String, shouldSuppressAlert: Bool = false){...}

This method started with on switch statement (and needs some refactoring now, so I won't show it as it still is one). In this statement all possible reactions are implemented. You might need a different return type to keep your state correctly in the app.

Lessons learned:

  • Although I thought that I have started big (different enums, central user alerting), the architecture could have been better (i.e. multiple classes, inheritance, ...).
  • I needed to keep track of previous errors (as some are follow ups) in order to only show one error message to the user -> state.
  • There are good reasons to hide errors.
  • Within the errorObj.userInfo map, it exits a user friendly error message and a technicalErrorMessage (which is send to a tracking provider).
  • We have introduced numeric error codes (the error domain is prefixed with a letter) which are consistent between client and server. They are also shown to the user. This has really helped to track bugs.
  • I have implemented a handleSoftwareBug function (which is almost the same as the handleError but much less cases). It is used in a lot of else-blocks which you normally do not bother to write (as you think that this state can never be reached). Surprisingly it can.

        ErrorHandler.sharedInstance.handleSoftwareBug("SW bug? Unknown received error code string was code: \(code)")

How does it look like in code: There are a lot of similar backend network requests where a lot of code looks something like the following:

func postAllXXX(completionHandler:(JSON!, NSError!) -> Void) -> RegisteringSessionTask {    log.function()    return postRegistered(jsonDict: self.jsonFactory.allXXX(),        outgoingMsgType: JSONClientMessageToServerAllXXX,        expectedIncomingUserDataType: JSONServerResponseAllXXX,        completionHandler: {(json, error) in            if error != nil {                log.error("error: \(error.localizedDescription)")                ErrorHandler.sharedInstance.handleError(error,                    msgType: JSONServerResponseAllXXX, shouldSuppressAlert: true)                dispatch_async(dispatch_get_main_queue(), {                    completionHandler(json, error)                })                return            }            // handle request payload            var returnList:[XXX] = []            let xxxList = json[JSONServerResponse_PARAM_XXX][JSONServerResponse_PARAM_YYY].arrayValue            .....            dispatch_async(dispatch_get_main_queue(), {                completionHandler(json, error)            })    })}

Within the above code you see that I call a completionHandler and give this caller the chance to customize error handling, too. Most of the time, this caller only handles success.

Whenever I have had the need for retries and other and not so common handling, I have also done it on the caller side, i.e.

private func postXXXMessageInternal(completionHandler:(JSON!, NSError!) -> Void) -> NSURLSessionDataTask {    log.function()    return self.networkquery.postServerJsonEphemeral(url, jsonDict: self.jsonFactory.xxxMessage(),        outgoingMsgType: JSONClientMessageToServerXXXMessage,        expectedIncomingUserDataType: JSONServerResponseXXXMessage,        completionHandler: {(json, error) in            if error != nil {                self.xxxMessageErrorWaitingCounter++                log.error("error(\(self.xxxMessageErrorWaitingCounter)): \(error.localizedDescription)")                if (something || somethingelse) &&                    self.xxxMessageErrorWaitingCounter >= MAX_ERROR_XXX_MESSAGE_WAITING {                        // reset app because of too many errors                        xxx.currentState = AppState.yyy                        ErrorHandler.sharedInstance.genericError(MAX_ERROR_XXX_MESSAGE_WAITING, shouldSuppressAlert: false)                        dispatch_async(dispatch_get_main_queue(), {                            completionHandler(json, nil)                        })                        self.xxxMessageErrorWaitingCounter = 0                        return                }           // handle request payload            if let msg = json[JSONServerResponse_PARAM_XXX][JSONServerResponse_PARAM_ZZZ].stringValue {                .....            }            .....            dispatch_async(dispatch_get_main_queue(), {                completionHandler(json, error)            })    })}

Here is another example where the user is forced to retry

        // user did not see a price. should have been fetched earlier (something is wrong), cancel any ongoing requests        ErrorHandler.sharedInstance.handleSoftwareBug("potentially sw bug (or network to slow?): no payment there? user must retry")        if let st = self.sessionTask {            st.cancel()            self.sessionTask = nil        }        // tell user        ErrorHandler.sharedInstance.genericInfo(MESSAGE_XXX_PRICE_REQUIRED)        // send him back        xxx.currentState = AppState.zzz        return


For any request, you get either an error or an http status code. Error means: Your application never managed to talk properly to the server. http status code means: Your application talked to a server. Be aware that if you take your iPhone into the nearest Starbucks, "your application talked to a server" doesn't mean "your application talked to the server it wanted to talk to". It might mean "your application managed to talk to the Starbucks server which asks you to log in and you have no idea how to do that".

I divide the possible errors into categories: "It's a bug in my code". That's where you need to fix your code. "Something went wrong, and the user can do something about it". For example when WiFi is turned off. "Something went wrong, maybe it works later". You can tell the user to try later. "Something went wrong, and the user can't do anything about it". Tough. "I got a reply from the server that I expected. Maybe an error, maybe not, but something that I know how to handle". You handle it.

I also divide calls into categories: Those that should run invisibly in the background, and those that run as a result of a direct user action. Things running invisibly in the background shouldn't give error messages. (Bloody iTunes telling me it cannot connect to the iTunes Store when I had no interest in connecting to the iTunes Store in the first place is an awful example of getting that wrong).

When you show things to the user, remember that the user doesn't care. To the user: Either it worked, or it didn't work. If it didn't work, the user can fix the problem if it is a problem they can fix, they can try again later, or it's just tough luck. In an enterprise app, you might have a message "call your help desk at xxxxxx and tell them yyyyyy".

And when things don't work, don't annoy the user by showing error after error after error. If you send then requests, don't tell the user ten times that the server is on fire.

There are things that you just don't expect to go wrong. If you download a file, and you can't put it where it belongs, well, that's tough. It shouldn't happen. The user can't do anything about it. (Well, maybe they can. If the storage of the device is full then you can tell the user). Apart from that, it's the same category as "Something went wrong, and the user can't do anything about it". You may find out as a developer what the cause is and fix it, but if it happens with an application out in the user's hands, there's nothing reasonable you can do.

Since all such requests should be asynchronous, you will always pass either one or two callback blocks to the call, one for success and one for failure. I have most of the error handling in the download code, so things like asking the user to turn WiFi on happen only once, and calls may even be repeated automatically if such an error condition is fixed by the user. The error callback is mostly used to inform the application that it won't get the data that it wanted; sometimes the fact that there is an error is useful information in itself.