Error handling seems to be controversial among developers. I have found many styles in my career and believe there are best practices that will be beneficial.
First, handle all your errors the same way. Don’t have different logic for different errors. I find throwing Exceptions to be the preferred method. You are able to create custom Exceptions and add additional information that will help you in debugging and triaging issues. The alternate method of returning error codes is dependent on the programmer checking for the error. Throwing exceptions on the other hand will always be noticed unless the programmer swallows the Exception in a catch.
Handling errors the same way makes reading code easier. It’s clearer if everything is in the try-catch versus a bunch of if statements inside the try and then the other errors in the catch statements. It also separates the normal logic of the code away from the error handling code. .Net has a perfectly good Exceptions framework that supports custom Exceptions for whatever your needs are. There is no need to introduce something else that is not part of the try-catch framework. Overall it is a cleaner way of coding.
Having an error code in the Exception can be helpful as well as you can make your custom Exceptions more robust and reusable. You can make your custom Exception generic enough as a category like Bad Data and then provide additional information in the error code such as what system or business logic the Exception occurred in. You could also customize your custom Exceptions to a particular application, system, or service. Overall, a custom Exception that is customized for a particular category and has an error code can be very reusable and can provide all the useful information that you need.
Although I haven’t successfully coded nor come across an application that does it, handling exceptions at the top of the stack seems to be the best way to process them.
You really cannot discuss error handling without considering Logging. Logging does create a log of noise in code as well as a lot of try catches. It seems best to collect Exception where needed but to log them at the top. If you do log at multiple levels in the stack I have found that you can provide a IsLogged property to an exception that is set once the Exception is logged and that way you do not have to log it again. Also, not everything needs to be logged in production. Use log levels and be very picky as to what you log as an Error level versus the other options. And the Critical level really should only be used for an application wide, urgent Exception.