Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handling errors in Swift

Tags:

In my application I need to download a JSON file from the web. I have made a ResourceService class that have a download method as seen below. I use this service in "higher level" services of my app. You can see there are multiple of things that may go wrong during the download. The server could be on fire and not be able to successfully respond at the moment, there could be go something wrong during the moving of the temporary file etc.

Now, there is probably not much a user can do with this other than trying later. However, he/she probably want to know that there was something wrong and that the download or the behaviour of the "higher level" methods could not succeed.

Me as a developer is confused as this point because I don't understand how to deal with errors in Swift. I have a completionHandler that takes an error if there was one, but I don't know what kind of error I should pass back to the caller.

Thoughts:

1) If I pass the error objects I get from the NSFileManager API or the NSURLSession API, I would think that I am "leaking" some of the implementation of download method to the callers. And how would the caller know what kind of errors to expect based on the error? It could be both.

2) If I am supposed to catch and wrap those errors that could happen inside the download method, how would that look like?

3) How do I deal with multiple error sources inside a method, and how would the code that calls the method that may throw/return NSError objects look like?

Should you as a caller start intercepting the errors you get back and then write a lot of code that differentiates the messages/action taken based on the error code? I don't get this error handling stuff at all and how it would look like when there are many things that could go wrong in a single method.

func download(destinationUrl: NSURL, completionHandler: ((error: NSError?) -> 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)             } catch let e {                 print(e)             }         } else {         }     }.resume() } 
like image 742
LuckyLuke Avatar asked Dec 18 '15 22:12

LuckyLuke


1 Answers

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      } } 
like image 162
Peter Hornsby Avatar answered Sep 25 '22 12:09

Peter Hornsby