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.
tryin 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).