Happy Path Error Handling

James Sumners
May 31 · 3 min read

The benefits and engineering learnings of a different pattern

Recently, one of our frontend engineers posted a link to a code pattern in our tech chat channel asking what everyone thought about it:

As it turns out, the backend team has been using a very similar pattern for quite some time, and we realized there could be some engineering learnings for the team as a whole. In particular, on the backend we write code like:

This pattern was partially inspired by Golang’s functions that return a result and an error, and partially by the way Joi returns results. The former because I have dabbled with Go and like the pattern, and the latter because we use a lot of Joi at Knock (so engineers already felt familiar with the result object {error, value}).

That’s all well and good, but what benefits does this pattern offer us over the traditional try/catch structure everywhere? After all, it’s basically a try/catch in another form.

Happy Path

Some time ago I watched a random YouTube video in which Mat Ryer presented his idea of aligning code such that the “happy path” is along the left edge of the screen. He writes about this in a Medium article titled, “Code: Align the happy path to the left edge.” In short, keeping the primary logic as close as possible to the left-most column of a function makes it easier to understand the function.

Using the {error, value} return pattern instead of try/catch makes it easier to adhere to this concept, and therefore to more quickly understand each other’s code.

Further, by reducing the proliferation of try/catch to promote this happy path concept, we reduce the temptation to wrap large blocks of code in one big try/catch. It’s a common anti-pattern wherein code that can throw in multiple locations is wrapped in one try/catch because writing multiple try/catch blocks are difficult to read and maintain. With the single try/catch, scope is lost on what causes the trapped error.

Error Code Bubbling

Using the{error, value} return pattern, we are also able to easily attach error codes to our error conditions. Consider the following set of example functions. First, we have a function that abstracts querying a database. The second function is a Fastify route handler. The final function is a “helper” function that implements business logic which invokes the database function. This function could also invoke other database functions or query other services for more data; each of these other potential functions would follow the same pattern of returning {error, code, value} instead of throwing.

By following this pattern of returning {error, code, value} instead of throwing errors, we can bubble error codes up to the route handler and define the error code closest to where the error was generated.

Testability

Another benefit to this pattern is it’s easier to write tests. Instead of having to trap functions for expected throws, which comes with its own set of legibility issues, tests can simply inspect the result as with a normal return condition.

I think it is fair to say that the throwing test is more difficult to read regardless of which throw detection method is chosen. On the other hand, the non-throwing test follows the happy path principle and is easy to read.

With dependency mocking, or dependency injection, testing becomes even easier. The inner functions being mocked have a contract for the result type they should return. Thus, a mocked function can easily return any desired condition so that all branches in the tested function can be covered.

Note: technically the functions.nonthrowing() function does not need to be an async function. It is merely defined so to be consistent with the topic.

Conclusion

By establishing a convention of returning {error, value} from our functions we have:

  1. Improved the legibility of our code bases
  2. Simplified returning errors to our consumers
  3. Reduced the complexity necessary to test our code

This makes it easy for team members to work on multiple projects and understand what others are doing. I have likely missed other benefits to this pattern in this article, but I hope I have made a decent case for why we have adopted it. Give it a try and let us know your opinion.

Follow the happy path and see the forest instead of the trees.

Interested in what we’re doing? Check out our list of openings and apply!

Knock.engineering

All things Knock Engineering & Data-Science.

James Sumners

Written by

Knock.engineering

All things Knock Engineering & Data-Science.