Handling Network Errors
iOS makes network communications relatively easy, but responding to all the types of errors and edge conditions that can occur is not easy. It is all too common to hook up networking code to see results quickly and plan on handling all the error conditions later. For nonmobile applications you can usually get away with this approach because the network connectivity from a workstation is predictable. If the network was there when the application loaded, it is almost always still there when the user loads the next page. In the rare case that it is not, the developer can depend on the browser to take care of displaying a message to the user. If you delay adding exception handling in a mobile app, you end up in a situation in which the network code needs significant refactoring as every new error case is encountered.
This section describes a design pattern that creates an elegant and robust framework for exception handling and requires little work to extend it for new errors in the future.
Consider the three major exception cases for mobile communications:
- The remote server is not reachable due to insufficient network connectivity from the device.
- The remote server returns an error response because of an OS error, HTTP error, or application error.
- The device attempts an unauthenticated request to a server that requires authentication.
As the number of potential exception conditions increase linearly, the amount of code required to handle them increases exponentially. If your code attempts to deal with each type of error on each type of request, the complexity and volume of your code also increases exponentially. This pattern attempts to bend that exponential curve back to a more linear curve.
Design Pattern Description
The pattern described in this section uses a Command Dispatch pattern combined with broadcast notifications. This pattern consists of the following object types:
- Controllers
- Command objects
- Exception listeners
- Command queues
The next section describes the behavior at a high level for each type of object.
Object Descriptions
This section describes the attributes and properties of the objects comprising the Command Dispatch pattern.
Controllers
Controllers are typically view controllers that request data and process the results. In this design pattern the controllers do not need to contain any exception handling logic. The only error cases they need to handle are a successful completion or a completely unrecoverable failure of the service. In the unrecoverable failure scenario, the controller would typically pop itself off the view stack because the user has already been informed of the failure by the exception listener objects described next. Controllers create commands and listen for the command's completion.
Command Objects
Command objects correlate to the different network transactions that the application performs. Examples of command object requests include retrieving images, fetching JSON data from a specifi c REST endpoint, or POSTing information to a service. A command object is a subclass of NSOperation. Because much of the logic in a command object is common to all other command objects, you can create a superclass command object to handle it and let specific commands inherit that logic. A command object has the following attributes:
- Completion notifi cation name - In iOS, controllers register themselves as observers for this notifi cation name. When the service call returns successfully, the command object uses NSNotificationCenter to broadcast a notification with this name. Although it is usually unique to the command class, in some situations it can be unique to a specific instance if there are multiple controllers issuing the same command types that want to distinguish individual responses.
- Server error exception notifi cation name - Special exception handler objects listen for this notification. The command object uses NSNotificationCenter to broadcast a message with this name when the server times out or returns an OS or HTTP error not related to authentication. All command classes usually share the same exception names, and therefore the same exception listeners. But different classes of commands may necessitate different exception listeners and have different server error exception names.
- Reachability exception notification name - The command object produces a notification of this type when it detects a lack of reachability to the Internet or a target host. Another exception listener may listen for this type of exception. In some apps this type of exception is not needed because the server error exception listener handles the reachability exceptions.
- Authentication exception notification name - The command object may produce a notification of this type if it determines the user is not authenticated or the server reports an unauthenticated status. A third exception listener waits for this type of notification to appear. The authentication notification names are typically shared across all notifications in the app.
- Custom attributes - These attributes are specific to the request being made. The issuing controller typically supplies these values because they are the specific business data needed for the service call and vary for each one.
Exception Listeners
Each exception listener is typically instantiated by the app delegate and remains in the background waiting for its specific type of notification. In many cases the exception listener displays a modal view controller when it receives a notification, which is described later in the Exception Listener behavior section.
Command Queue
Controllers submit commands to the command queue for processing, and an app may have one or more command queues. In iOS, command queues are subclasses of NSOperationQueue. The main queue should not be used as a command queue because its operations run on the user interface thread, which impairs the user experience when executing long-running operations. Using NSOperationQueues provides built-in capability for managing the number of active operations and interdependencies between operations.
Object Behaviors
Each of these objects has a distinct part to play in successfully completing a network transaction. The following section describes their respective roles in this pattern.
Controller Behaviors
Controllers are focused on executing UI and business logic. When a controller wants data from a service, it should take the following actions:
- Create a network command object.
- Initialize the request for specific attributes of the command object.
- Register as an observer for the completion of the command.
- Push the command onto an operation queue for execution.
- Wait for the NSNotificationCenter to deliver a completion notification.
When the operation completes, the controller receives a completion notification and takes the following actions:
- Checks the status of the operation to see if it was successful.
- If successful, it processes the received data. The received data is supplied to the controller via the userInfo attribute of the NSNotification object. NSOperationQueues execute NSOperation objects on their own thread. When the operation completes it sends an NSNotification via the NSNotificationCenter. The notifi cation callback methods are called on the thread on which the NSOperation runs, which in the example ensures that it arrives on a thread other than the main thread. If the controller manipulates the UI, then it needs to make those changes on the main thread, usually via Grand Central Dispatch (GCD).
- If unsuccessful, the controller has a number of options depending on the application requirements. For example, it may pop itself off the view stack or update the UI, indicating that the data is not available. It should not ask to retry or show a modal alert because those actions are the responsibility of an exception listener.
- The controller should unregister itself as an observer for the commands' completion notification. In some cases this is not desirable if the controller wants to monitor for other data arriving from the same command type.
Notice that controllers do not have any logic to handle retries, timeouts, authentication, or reachability; that logic is all done by the commands and exception listeners.
If the controller wants to guarantee that only it receives the returned data, it should alter the completion notifi cation name for that instance of the command object to be a unique value prior to placing it on the queue and listen for notifications of that unique name.
Command Object Behaviors
Command objects are responsible for calling the target service and broadcasting the results of that service call. The steps generally taken by a command object follow:
- Check for reachability. If the network is not reachable, broadcast a reachability exception notification.
- Check for authentication status if required. If the user is not yet authenticated, broadcast an authentication exception notification.
- Build the network request using the custom properties provided by the controller. Usually the endpoint URL is specified as a static attribute of the command object class or loaded from a confi guration subsystem.
- Issue the network request using a synchronous request.
- Check the status of the request. If the status is an OS or HTTP error, it broadcasts a server exception notification. If the error is an authentication error, it broadcasts an authentication exception notification.
- Parse the results.
- Broadcast a completion notification with a successful status.
When a command object broadcasts a notification, completion or otherwise, it needs to create a dictionary object that contains a copy of itself, the status of the call, and any data returned as a result of the call. The self-copy is necessary because an instance of an NSOperation can be executed only once. As discussed in the next section, a command may be resubmitted when the listener handles the exception.
The synchronous request API is ideally suited to this pattern because the commands are executed on a background thread instead of the main thread. If the request transmits or returns a larger amount of data than you want to squeeze into memory, your application needs to use asynchronous requests. Because the main function of an NSOperation is a single method, the operation must implement concurrency locking to block its main method until the asynchronous call completes.
Exception Listener Behaviors
Exception listeners are the magic that makes this pattern especially powerful. These objects are usually created by the app delegate and remain in memory listening for notifications. When a notification is received, it is the responsibility of the listener to inform the user and potentially solicit a response from the user (other than throwing the phone through a wall). In the case of an exception, the notification contains a copy of the command that triggered the exception, and after the user has responded, the listener usually resubmits the command back onto the queue to be retried. One interesting caveat for the exception listeners is that because multiple commands may be in-flight there may be multiple exception notifications generated while the user is still responding to the first exception. Because of this, the exception listeners must collect exception notifications and resubmit all the triggering commands after the user responds to the first exception. This collection of errors prevents a common form of app misbehavior where the user is bombarded with UIAlertViews triggered by the same fundamental problem.
The flow for a server exception can be as follows:
- Present a nice looking modal dialog explaining the error and giving the user the option to cancel or retry.
- Collect any other server exceptions that may be broadcast.
- If the user selects retry, dismiss the dialog and resubmit all the collected commands.
- If the user selects cancel, dismiss the dialog. The listener should set the command completion status to failed for all the collected commands and ask each one to broadcast a completion notification.
The flow for a reachability exception may be as follows:
- Present a nice looking modal dialog informing the users they need to be on a network with connectivity.
- Collect any other service exceptions that may be broadcast.
- Listen for reachability changes. When the network is reachable, dismiss the dialog and resubmit the collected commands.
The flow for an authentication exception is a bit more complicated. Keep in mind that commands are independent of one another, and many can be in flight at any one time. The authentication flow does not generate authentication exception notifications. The flow may look like this:
- Present a login view modally.
- Continue collecting commands that failed due to authentication errors.
- If the user cancels, the listener should send a completion notification for the collected commands with a failure status.
- If the user provides credentials, create a login command, and place it on the command queue.
- Wait for a completion notification from the login command.
- If the login didn't succeed due to a username/password mismatch, return to step 2. Otherwise, dismiss the login view controller.
- If the login command was successful, resubmit the triggering commands to the command queue.
- If the login command failed, then ask the triggering commands to send a completion notification with a failure status.
Command Queue Behaviors
Command queues are native iOS NSOperationQueue objects. By default a command queue operates in first-in-first-out (FIFO) order. When your code adds a command object to an NSOperationQueue it performs the following actions:
- Retain the command object so that its memory will not be released.
- Wait until an available slot comes open at the head of the queue.
- When the command object arrives at the head of the queue the start method of the command object is invoked.
- The main method of the command object is invoked.
Refer to iOS API documentation on the NSOperation and NSOperationQueue objects for detailed information on the interaction between the queue and the command objects.