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
downloadTaskWithRequest. If you pass back some other error then your caller is left wondering what really happened. If you pass back the error you got then they know