There are many well-known best practices about exception handling in isolation. I know the "do's and don'ts" well enough, but things get complicated when it comes to best practices or patterns in larger environments. "Throw early, catch late" - I've heard many times and it still confuses me.
Why should I throw early and catch late, if at a low-level-layer a null pointer exception is thrown? Why should I catch it at a higher layer? It doesn't make sense for me to catch a low-level exception at a higher level, such as a business layer. It seems to violate the concerns of each layer.
Imagine the following situation:
I have a service which calculates a figure. To calculate the figure the service accesses a repository to get raw data and some other services to prepare calculation. If something went wrong at the data retrieval layer, why should I throw a DataRetrievalException to higher level? In contrast I would prefer to wrap the exception into a meaningful exception, for example a CalculationServiceException.
Why throw early, why catch late?

In my experience, its best to throw exceptions at the point where the errors occur.  You do this because it's the point where you know the most about why the exception was triggered.

As the exception unwinds back up the layers, catching and rethrowing is a good way to add additional context to the exception. This can mean throwing a different type of exception, but include the original exception when you do this.

Eventually the exception will reach a layer where you are able to make decisions on code flow (e.g a prompt the user for action). This is the point where you should finally handle the exception and continue normal execution.

With practice and experience with your code base it becomes quite easy to judge when to add additional context to errors, and where it's most sensible to actually, finally handle the errors.

Catch → Rethrow

Do this where you can usefully add more information that would save a developer having to work through all the layers to understand the problem.

Catch → Handle

Do this where you can make final decisions on what is an appropriate, but different execution flow through the software.

Catch → Error Return

Whilst there are situations where this is appropriate, catching exceptions and returning an error value to the caller should be considered for refactoring into a Catch → Rethrow implementation.
