Client side error handling techniques

Handling errors on the client side can be quite a daunting task as your codebase grows. Errors can come from many places, many of which may not be even in your control. I would like to share a few techniques that have helped me develop consistent and predictable apps even when things are going wrong. Most of these principles can be applied in any sort of client application whether it be a web or mobile app.

Use the most specific messaging possible

We have all experienced apps that use generic messaging such as “oops something went wrong” whenever something goes wrong. It is easy and tempting to have a single message used across your entire app but this can drastically reduce the quality of your app for a few reasons.

  • It makes the user feel helpless, which then sometimes leads them to mash the button again which can cause more problems for both them and you
  • It can make it very difficult to understand where and when things are failing
  • The two scenarios above then compound even further increasing the time it takes to fix because often if you are using generic messaging you are likely not properly instrumented in the first place 😂

So the question becomes what do you do? I would suggest using the most specific language you can. Try to tell the user something failed trying to do XXX or we were unable to XXX.

Lets say you are building an app that has number of create, read, update, or delete operations. Instead of using a single generic message try to construct a message that is specific to the operation at that given moment. You might use messages like Unable to update user or Unable to create reminder. This gives the user something specific to report back. This gives you the developer a much better chance at understanding where exactly something might be failing. In addition if you have multiple calls chained together then you end up with a better understanding of where in the call chain things are breaking down.

Keep messages and logic close to the source of the error

Whether your own code is throwing an error or you are experiencing a failure from a 3rd party library I have found it best to keep the error as close to the source as possible. I prefer to make my UI code error handlers simply show whatever error message they get.

The most common place I see this go wrong is with networking clients. It is very easy to leak HTTP semantics into the UI code in order to show different messages based on the status code. Even if you do not own the server responses try to keep the network call and the error handling at the call site. Lets take a look at an example of a network client trying to authenticate a user.

function login() {
	return new Promise((resolve, reject) => {
  		let response = API.login()
		switch (response.status) {
			case 200: resolve(response.body.result)
			// Handle all error cases here and return unique messages
			case 401: reject("Your credentials were invalid. Please try again.")
			case 400: reject("The login request was invalid")
			case 500: reject("Our server was unable to process the login request")
			else: reject("An unexpected error occurred during login")
		}
  	});
}

login().then(function(success) {
   //Proceed to the app
})
.catch(function(error) {
  displayError(error.message);
});

A few things to notice in this snippet.

  • There is no logic in the error handler. It shows whatever message it receives
  • Each error scenario is handled right next to the API client call
  • Every response has a unique message to help you pinpoint the potential code path

This can help you handle cases where just showing a message is appropriate but often times we need our UI code to react more intelligently. What do we do in that case?

Model your application state when an error message is not sufficient

When we need to do more than show a message we might be tempted to put this application logic in the error handler. Lets expand on our login function above to incorporate more specific scenarios.

Lets assume our API client documentation states that after too many login attempts the system returns a 429 to indicate to the client requests should be throttled. How would we indicate this to the user and also disable the button in the UI? In this case we do not want to leak the HTTP semantics but we need an indicator downstream to explicitly handle this case.

When you encounter a situation like this I have found it helpful to model the various states as an enum or sealed class. These are constructed at the call site and helps signal to the consumer the scenarios I expect it to handle. Our login example might be expanded to look like this.

enum LoginStatus {
	Success,
	InvalidCredentials,
	TooManyAttempts,
	Failed
}

class LoginResult(message: String, status: LoginStatus, user: User?)

function login() {
	return new Promise((resolve, reject) => {
  		let response = API.login()
		switch (response.status) {
			case 200: resolve(LoginResult("Ok", Success, response.user))
			// Handle all error cases here and return unique messages :)
			case 401: resolve(LoginResult("Invalid credentials", InvalidCredentials, null))
			case 400: resolve(LoginResult("We were unable to complete your request", Failed, null))
			case 500: resolve(LoginResult("We were unable to process the request", Failed, null))
			case 429: resolve(LoginResult("Too many login attempts. Please wait 1 minute", TooManyAttempts, null))
			else: reject("Unable to authenticate")
		}
  	});
}

login().then(function(result) {
   	//Handle the various states below
	//Success, InvalidCredentials, TooManyAttempts, Failed
})
.catch(function(error) {
  	//This should be an exceptional case. Just show the message.
  	displayError(error.message);
});

In the code above I have transformed the result of the login function to be an object representing the various states the app could be in. I use an enum (or sealed class in Kotlin) to ensure the downstream consumer has a map of what potential states it needs to handle. I have also moved the object returned into our applications success path rather than rejecting the promise and signaling an error occurred.

This may come as a surprise to people but when I start modeling the different potential states I no longer consider them to be errors. They become explicit scenarios that I expect the downstream consumer to handle exhaustively. This keeps the logic in our standard application path and ensures the error handling is kept only to display simple messages to the user.

Final thoughts

The ideas above are not exactly new and they are by no means the “right way” to do things but I found them to be valuable when dealing with client side errors. In implementing these concepts I find my apps to be more robust and resilient when things go wrong and have found it easier to track down problems when they do arise unexpectedly.