Five Steps to Great Error Handling

Garrett James Cassar
Nerd For Tech
Published in
6 min readMar 1, 2021

A crucial element to any serious business is clarity in communication.

Like most things in engineering, this is hard. It involves anywhere between a handful and hundreds of engineering teams, all trying to have a clear and informative conversation amongst themselves and to their customers about what is going on within their services and what they can do next. Hard.

While achieving effective communication is mostly a human and organisational exercise, your technology stack can and should enable this good behaviour. This blog post proposes a system that can address a huge slice of the communication pie. Error handling.

Exception Handlers
Achieving consistency around error handling is not easy and often ends in code duplication, fat microservices, inconsistent messaging and potential PII leaks. Eek!

But with a bit of clever design, tackling this problem presents a quick win and a great opportunity to standardize your messaging, keeping your microservices small and simplifying your developer’s lives.

Exception handlers translate exceptions into messages.

@ExceptionHandler(NotCuteEnoughException.class)
ErrorResponse handle(NotCuteEnoughException e) {
return ErrorResponse
.builder()
.reason(ErrorCode.PET_NOT_CUTE_ENOUGH.value())
.errorMessage("ya dog ugly")
.build();
}
@ExceptionHandler(TooFluffyException.class)
ErrorResponse handle(TooFluffyException e) {
return ErrorResponse
.builder()
.reason(ErrorCode.TOO_FLUFFY)
.errorMessage(”ya dog stinky”)
.build();
}

Solutions like this are common. They work and might fit all the acceptance criteria for your ticket. What happens if we get an error that is not related to your dog being not cute enough or not fluffy enough?

You would need a new exception handler, that produces a new arbitrary error response and error response format. And the next exception? Another.

It’s difficult to state in a short blog post how unscalable this approach really is. What happens if I decide to follow a CPRS pattern and split the microservice into two? That’s a potential duplication. What happens if we need to call an external “Pet Verification Service” that call is made by 8 Microservices? Another set of 8 duplications. I could go on for a while, but the point is it gets big.

Step 1: Reference data from your Exceptions
The first and most obvious point here is actually that your exception can hold data. By your exception and making it more generic, we can quickly collapse the three exception handlers into one.

class PetAintRightException extends RuntimeException { 
ErrorCode errorCode;
PetAintRightException(){
super("Somethings not quite right!");
this.errorCode= ErrorCode.PET_AINT_RIGHT;
}
}
@ExceptionHandler(PetAintRightException.class)
ErrorResponse handle(PetAintRightException e) {
return ErrorResponse
.builder()
.reason(e.getErrorCode())
.errorMessage(e.getMessage())
.build();
}

Now, we only have one method to handle all of my pet-related issues and do not need to create a new exception handler each time I encounter a new pet-related error. But it’s not that descriptive, is it? What actually went wrong here? Was the Pet not cute enough? Not fluffy enough? Too fluffy? Who knows?

By using a generic exception, it’s clear that we can’t really get a more specific error message or error code.

Step 2: Use inheritance to organise your exceptions
Using inheritance for exception handlers has several major advantages.

We can use inheritance to target the same handler for different exceptions.

--- Exceptions ---
abstract class PetAintRightException extends RuntimeException {
ErrorCode errorCode;
PetAintRightException(String message, ErrorCode errorCode) {
super(message)
this .errorCode = errorCode
}
}
class PetNotCuteEnoughException extends PetAintRightException {PetNotCuteEnoughException (String pet) {
super(pet + “not cute enough!”), ErrorCode.PET_NOT_CUTE_ENOUGH)
}
}
class PetNotFluffyException extends PetAintRightException {PetNotFluffyException(String pet) {
super(pet + “is not fluffy enough!”), ErrorCode.PET_TOO_BALD)
}
}
---- Handler ---@ExceptionHandler(PetAintRightException.class)
ErrorResponse handle(PetAintRightException e) {
return ErrorResponse
.builder()
.reason(e.getErrorCode())
.errorMessage(e.getMessage())
.build();
}

Nice! Now the handler translates exceptions into errors without having to hard-code anything!

As you can see above, we get a consistent message format by sharing a handler but keep control of the message with the custom exceptions.

While this solution has fewer methods and looks a lot nicer, it’s still not perfect. What if the REST endpoint that we’re exposing is public-facing and we need to hide the fidos PII?

Step 3: Target your inheritance structures according to the audience

abstract class PetCustomerException extends PetException {PetCustomerException(String pii, String message,ErrorCode ec) {
super(RandomizationService.randomize(pii) + message);
this .errorCode = errorCode;
}
}
abstract class PetStoreOwnerException extends PetException {PetStoreOwnerException(String pii, String message, ErrorCode ec) {
super(pii + message)
this .errorCode = errorCode;
}
}
PetExpiredCustomerException extends PetCustomerException {

PetHasExpiredCustomerException(String name) {
super(name, “ has moved to a nice farm up state!"), ErrorCode.PET_MOVED_UPSTATE);
}
}
class PetExpiredStoreOwnerException extends PetStoreOwnerException {

PetHasExpiredStoreOwnerException(String name) {
super(name, “ has expired!”, ErrorCode.PET_DEAD);
}
}

This solution is slightly bigger, but it is unavoidable. Protecting details from a front-end client is not nice to have. If you think about it, the handler only has one job. To inform the recipient of the message of what went wrong and what to do about it. So having one handler for each type of audience represents excellent cohesion. While these errors divide nicely into “client-facing”, “non-client facing”, there’s once again no reason to stop there. You could easily break this down into “StoreToStoreExceptions, WebsiteExceptions, MobileExceptions, ContentManagerExceptions” and as many types of clients that you can dream up.

But there is still an elephant in the room (pun intended). What if we need to verify the pet id externally? Or translate external ids to internal ids? And what if we call this service from 15 different microservices around the company?

Step 4: Publish your abstract exceptions and exception handlers as a sharable library

Error handling is a great candidate for a shared library at many levels, which I can discuss at length in another article. It has great cohesion, minimal dependencies, loose coupling and unopinionated. It also represents something that every microservice needs to do and controls something that as a developer you don’t really want to care about, but easily regain control over by overriding if you really need to.

With a central error handling library, at minimum what you get is the option for every microservice to conform to the error handling message formats specified by the company. At maximum what you get is a super clean development experience that gives you maximum control with minimal effort, a shorter product discovery life-cycle, and a familiar format for all the teams in your organisation.

Step 5: Categorize your errors across your organisation
In a shared library, it becomes easy to find a meaningful set of error codes similar to HTML.

PS-4041 Pet not found because it ran away (PS-4041 runbook)
PS-4042 Pet not found it was taken already (PS-4042 runbook)
PS-4043 Pet not found because its living with a nice farmer up state (PS-4043 runbook)

This helps create semantics, link errors to your run-books, and create a consistent message across the entire organisation. While that process can be hard to manage across an organisation, once it’s in place it really helps to create clarity for your customers and within the company.

Conclusion

Implementing an error handling library on an organisational level gives you:

  1. Leaner, sexier microservices
  2. Less work for your backend programmers, product owners and team leads
  3. Better grouping and semantic meaning of issues within an organisation
  4. More consistent communication with your clients and within your company.

Please share this with all your Medium friends and hit that clap button below to spread it around even more. Also, add any other tricks that you use to keep microservices smaller!

Add me on LinkedIn here if you liked this article or want to give me lots of money.

--

--