6

I have been having this dilemma from time to time whenever I have to design or start a new project from scratch.

I particularly like how Spring framework is organised in terms of classic software patterns like services, repositories, etc. By reading and going through community projects written in Spring and for example Node.js (two absolutely different environments), the APIs differ in several points but the one this post is about is exception handling.

Imagine we are hitting a specific resource by HTTP request. It will first go to the controller, assuming no middleware in place, service, repository if any and back to the controller. But what happens if the service needs to throw an error or the custom implementation of the repository needs to throw an error?

I have seen implementations of both types:

  1. Inside the service/repository, throwing the actual HTTP error wrapped in a try catch statement like: new BadRequestException(statusCode, message)
  2. Let the exception or error bubble up until a global middleware catches it, and returns it back to the client.

I personally believe that mixing the logic layer (service) and data layer-ish (repository) with the transport layer (HTTP controller) is a mistake or a wrong choice since you are mixing conceptually different things. And on the latter, reliance on a global middleware will make you specify the status code or instantiate a class that internally uses the needed status code and message, again, no point on there.

In frameworks or environments that different such Spring and Node.js, I've seen both implementations, especially on Node.js. Nest uses more like a Spring approach and Express more like the second one.

What would you rather do or which is your preferred way of handling this exception handling?

4
  • 1
    If your services and repositories are throwing errors with knowledge about the transport layer, there's no point separating the transport layer. The service should throw a business-level error, then the transport layer turns that into e.g. an HTTP response. This doesn't have to be a global middleware, it could just be try in a method in the controller (or see e.g. baeldung.com/exception-handling-for-rest-with-spring for some options when that needs to be across multiple methods).
    – jonrsharpe
    Jun 14, 2019 at 7:03
  • 1
    All your efforts of "decoupling" or "abstracting" one layer from another or all your efforts on separating concerns are just a waste of time if you implement #1. If Repositories are meant to be agnostic to all other layers, why it should be aware of HTTP status and other HTTP stuff? Why it does know it was called from a controller? It should not even know that the whole transactions belong to a higher abstraction named HTTP request. It does not make much sense IMHO.
    – Laiv
    Jun 14, 2019 at 7:28
  • @Laiv totally agree. The fact of picturing an example in the repository layer was an extreme scenario where as you point, it doesn't make any sense at all. It's just stating the fact that I've seen that approach on community projects Jun 14, 2019 at 7:32
  • It could make sense if, as Springs automagically allows you, you decide to turn your repository interfaces into the REST interface too. This is possible through the Spring data project. In such a scenario, you don't declare controllers. These are created automagically by spring. But then, the question is not which error handling strategy is preferable. The question is "why such a design?".
    – Laiv
    Jun 14, 2019 at 7:49

2 Answers 2

8

Inside the service/repository, throwing the actual HTTP error

I agree with you, this doesn't belong here.

The service layer shouldn't know its consumer. Your services might be consumed by a web application today, but I should be able to start consuming them from e.g. a WPF application tomorrow, without changing the service code. When your services throw HTTP exceptions, that means your code is not reusable.

What's happening here is that the exception type is being abused as a message to the end user. That is not what the type should be used for.

However, what is correct is that you would throw different exception types based on the actual problem, and your web project can map this exception's type (or data) to a specific HTTP status code.

Let the exception or error bubble up until a global middleware catches it, and returns it back to the client.

I agree with this, except the (maybe too pedantic) mention of returning it (i.e. the exception) to the client. You shouldn't return the exception, but rather use the exception to generate an appropriate message.

That message could be an exception's details (which I often do in DEBUG mode, but never allow in RELEASE), or it could be a default "An error has occurred" message, or it could be a custom designed object of your own choosing. Pick what is appropriate for your situation.


As a real world example, we implemented a PublicException class (the name was different, but changed due to privacy reasons) in one of our projects. If an exception was of this type (or a derived type), its message would always be displayed to the user. This was by design, because some messages needed to actually be returned from the business layer to the consumer. This was part of the requirements/analysis that was given to us, so we had no choice in the matter.

This was set up as follows:

  • If it's a PublicException, return the exception message to the user.
  • In a RELEASE build, any other kind of exception would return a basic "An error has occurred. Contact your admins and give them this reference: xxxxxx" message
  • In a DEBUG build, any other kind of exception would have its message returned to the user, because this was assumed to be a local debug session by a developer.

All of our controllers had some middleware on them which caught any unhandled exceptions, and would format the return message (and HTTP status code) to the user.

The HTTP status code was decided based on the exception type. PublicValidationException leads to 400 bad request responses, non-PublicException exceptions to 500 internal server errors, and so on.


As a bare minimum (and that's a notable caveat), I believe that you only really need to have a last line of defense against exceptions, i.e. catch them on the controller level (or ideally some middleware in your web project). This last line of defense ensures that exceptions don't bleed to the consumer, because you should avoid this at all costs.

But that doesn't mean that your codebase should be the Wild West in terms of exception throwing. Exceptions should be avoided as much as possible, but you need a last line of defense in case one slips through.

Your business (and data) logic should avoid exceptions as much as possible. Exceptions are expensive, and it's generally much more efficient to return a pass/fail return value than it is to throw an exception. If exceptions are commonly expected, then they are not exceptional, and therefore should not be exceptions.

In cases where the exception effectively ends the life of the web request (you cannot continue because of the issue), just let it bubble up to the surface and let the middleware deal with it. Note of course that you can still enrich your thrown exception when it adds value.

If there are exceptions that would not end the life of the request (e.g. exception that a non-essential IO device is offline), then you have to of course handle these exception in your codebase. Where to do so is completely dependent on the work that should resume in case the exception is thrown.


So, to summarize:

  • Do not let your projects know who consumes them. HTTP details should only be known inside web project.
  • Always have a last line of defense against exceptions. Preferably as close to the end user as possible (= controller), and a middleware approach is often nice to use as a blanket line of defense.
  • Avoid using exceptions where possible. Always prefer returning values over exceptions. Exceptions should be, wel, exceptional.
  • If the current web request cannot recover from this exception, then let it bubble up (after maybe enriching it, where needed) to your last line of defense.
  • If the current web request can recover from this exception, then handle it at the appropriate location, i.e. where to resume the code execution from.
1

Our Spring project is designed to have a top level handler (with @ControllerAdvice and @ExceptionHandler) that makes sure that all kinds of exceptions are handled in some sane way. This allows us to have specific exceptions that map to HTTP codes, such as UnrecoverableException which will bubble up and return a 500 Internal Server Error.

Low level exception handlers can then decide whether an exception from a 3rd party or other code means that the server is most likely incapable of servicing requests, or whether it might be a temporary issue (at which point a RecoverableException -> 503 Temporarily Unavailable is thrown).

So the general exception handling becomes straight-forward, if we can't do anything about an exception, we map it to a suitable "supertype" and let it go upwards. The supertype exceptions can then contain a lot of additional information for things like logging purposes, method parameters that caused the exception, the current state of things etc.

It works well in our architecture, and keeps exception handling localized. Either you handle the exception, or you send it to the top. But you avoid sending it to the direct parent in the classic "well I can't handle this, but I hope my caller can" way (well not strictly, but it's not so much of a guess where a specific exception might get handled).


Spring also provides a way to map an exception directly to a HTTP status, with @ResponseStatus in the exception class, but what if we're not using HTTP? With our solution you can create a gateway specific @ExceptionHandler because the exception only describes the state of the system and intent of the error, not what the end result should be.

This is of course Spring specific, but it may be achievable in other languages / frameworks.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.