The Error Handling Done Right

And it will save more time in the future

Liu Tao
The Startup
5 min readJul 15, 2019

--

I skim through this topic in this post: separate business logic from error handling. But this topic seems well worth dig much deeper. I have seen in many codebases, error handling is just afterthought of the design phase and most likely are just randomly plugged in. This post will give some thought food about why and how of error handling in our daily practice.

It’s all about signal/noise ratio in your code

As programmers, we use code to solve problems for our business. If we think harder, write less code, our business will bear less maintenance cost in the long run. In most cases, error handling code won’t provide business values. On the other side, the error-handling code is more complicated than happy path code and is difficult to get it right. It must be safe, and the nested error could be worse.

Use different channel for business logic and error handling

As mentioned in the previous post:

In the ideal world, all your code should be only about happy path. Frameworks, libraries and annotation can handle all the errors for you.

But we are not there yet, no matter how hard we try, we still need to write some error handling code. Error should be the first-class citizen in our codebase and should have its own channel. Traditionally we will have the error check and business logic in the same block:

if (dto != null) {
result1 = processA(dto);
}

if (result1 != null) {
result2 = processB(result1);
}

if (result2 != null) {
result3 = processC(result2);
}

return result3 != null;

The more preferred approach is to have separate channels for business logic and error handling logic. Java’s unchecked exception can serve as such an error handling channel. In the following code snippet, each function throws the same exception MyBusinessException:

try {
result1 = processA(dto);
result2 = processB(result1);
result3 = processC(result2);
} catch(ApplicationExeption e) {
//handling exception here
}

We can see that the business logic and error handling logic are separated. But with the exception, we have to construct the error handling channel using clunky exception syntax.

Java 8’s Optional provide the built-in channel for such error handling.

Optional.ofNullable(dto)
.map(this::processA)
.map(this::processB)
.map(this::processC)
.map(r -> r != null)
.orElse(false)

From the above three examples, the traditional way of null pointer check definitely has the lowest signal/noise ratio. Optional and Exception has similar signal/noise ratio. But arguably the Optional way is more eye-friendly for trained professionals. Also, it provides one convenient way to return a default value. With Exceptions, this can only be done in the catch block. One advantage with exceptions is that we can throw Exception across multiple layers easily, and sometimes this can help us get the clean code.

Another view to understand those difference is from the explicitness point of view. A separate error handling channel is much more explicit than error checks in the middle.

Be aware of error’s abstraction level

Whenever we write any code, we should be mindful of the abstraction level of our code. The same applies to error handling. When we report any errors, we need to think whether our up layer can make sense of this error and what they can do about it. We should not leak the implementation detail of our code. When applies this principle to Java’s exception, we will translate the low-level exception to exceptions that can be explained in higher-level abstractions, then the higher-level logic is not coupled with low-level exceptions.

public class AppExeption extends RuntimeException {
public ApplicationExeption(final String msg) {
super(msg);
}
public AppExeption(final String msg,
final Throwable cause) {
super(msg, cause);
}
public AppExeption(final Throwable cause) {
super(cause);
}
}

Also, exceptions can serve both as error handling strategy and communication mechanism in your system. The whole department should have the same understanding of its best practices and apply them consistently in the entire system.

Error-handling design is part of API design

As a guideline for any API design, we want our API has the smallest surface. This is the primary reason that checked exception is considered bad practice for most of the business application. It just bloats our interface unnecessarily.

Exceptions should be carefully designed and should have a meaningful business meaning. Programer should not just throw exceptions on anything suspicious. Instead, all error scenario should be thought thoroughly, and our business has the final say about how an error should be handled.

In general, we should try our best to handle errors in our layer. Throwing exception should be the last resort to notify other components to give a hand.

The strategies to handle error

Whenever we define an error, we should think through whether the caller has any way to handle it. If the caller has no way to handle it and it’s a critical one, we can log and let the application crash. If it’s not critical, we can try our best to return value that’s still meaningful to the caller and no additional overhead on the caller side. For example, for any code that should return a list, if somehow we can’t get the list that caller want, there are three ways to handle it:

  • Return null: this shifts the null check responsibility to the caller implicitly. Somewhere else must document the returned value could be null and the caller must handle it.
  • Return Optional: this is better than null. It models the valid absence of value explicit. No additional documentation required.
  • Return empty list: this is the preferred approach if our business agrees with it. No special handling required.

This is another general principle for system design. We want any chunk of code has clear boundaries of responsibility. Beyond that, we want to lessen the complexity of our client. That’s the reason that empty list is the preferred approach for error handling.

Avoid Simple Log and Rethrow

A lot of codebases I worked with still have a lot of simple logging and rethrow for exception handling. First, this indicates that the whole organization doesn’t share the same understanding for exception handling. Second, if logging and rethrow is needed in some case, it can be done by annotation. A company-wide logging framework can be developed for this purpose.

Null is a code smell

It’s now a common sense that null is at least code smell in software, if not a design error. It represents the wrong way to model the absence of a value in a programming language. Optional in Java 8 make this absence explicit. It also provides constructs like map/filter to retrieve what you want with an easily comprehensible statement, without using the high cognitive-load conditional branches.

Use retry mechanism for external dependency

For any external dependency, we usually have facades or DAL layer to abstract them away from our business logic. To avoid temporary glitch of those dependencies, we can define a retry strategy using a framework like spring-retry. The system is more robust, and the business logic is not coupled with the low-level retry mechanism.

Summary

A lot of design strategy is to have each component independent and explicit. For error handling, if we can’t handle it by ourselves, we should at least make it explicit, so our client has no chance to miss the responsibility on their part.

--

--