As a project gets bigger and bigger, I get a bit confused as to what types of exceptions should be thrown and where they should be caught, e.g. how to organize internal exceptions vs exception messages that should be shown to the end user. To keep confusion down, is it best practice to always catch an exception in the higher-level object?
Take this example: if I have a database class that inserts a row into the database, and that class is called by another object that processes data, and in turn this data processing object is called by a controller, is it correct to catch errors from the database class in the processing class, which (possibly) rethrows the error to be caught in the controller class? Or can an error thrown in the database class be caught in the controller class?
Additionally, if one method in the processing class is called by another method in the same class, and the first throws an error, is it ok to catch the exception in the same class? Or should it be deferred to the higher-level class that's calling it?
Are there any other tips on how to structure and organize exceptions in large projects with many levels of classes?
I like to think about exceptions as expected events that may interrupt your normal program flow but in an orderly and controlled way. The task of your exception handling is to deal with this situation and resolve it in such a way that your program state remains valid and the application can continue.
Therefore you should add the exception handling at the first place up the calling hierarchy where you are actually able to resolve the situation. This may include cleaning up previously opened resources which are now no longer needed, logging the event or providing feedback to the user.
In your example I would probably leave the handling logic to the controller. The database often does not have enough context of what has just happened and how to deal with specific conditions since those depend on the context in which the database has been called. Your controller on the other hand should have all context information and should be well aware of what the program just tried to do. It is also probably better suited to resolve the issue for example by displaying a general error message to the user and maybe send detailed error report to the administrator.
Sometimes you will also have the situation where you need to catch exception on an intermediate level, to do some cleanup (like closing streams or rolling back some actions) and then rethrow the exception because you know that you did only resolve part of the situation.
All in all may general recommendation is to think about what actions need to be done to resolve such an exceptional event and then implement the error handling where those actions can be done easily.