I've seen (and held) various strong opinions about this. The answer is that I don't think there currently is an ideal approach in C#.
At one point I felt that (in a Java-minded way) the exception is part of the binary interface of a method, as much as the return type and parameter types. But in C#, it simply isn't. This is clear from the fact that there is no throws specification system.
In other words, you can if you wish take the attitude that only your exception types should fly out of your library's methods, so your clients don't depend on your library's internal details. But few libraries bother to do this.
The official C# team advice is to catch each specific type that might be thrown by a method, if you think you can handle them. Don't catch anything you can't really handle. This implies no encapsulation of internal exceptions at library boundaries.
But in turn, that means that you need perfect documentation of what might be thrown by a given method. Modern applications rely on mounds of third party libraries, rapidly evolving. It makes a mockery of having a static typing system if they are all trying to catch specific exception types that might not be correct in future combinations of library versions, with no compile-time checking.
So people do this:
try
{
}
catch (Exception x)
{
// log the message, the stack trace, whatever
}
The problem is that this catches all exception types, including those that fundamentally indicate a severe problem, such as a null reference exception. This means the program is in an unknown state. The moment that is detected, it ought to shut down before it does some damage to the user's persistent data (starts trashing files, database records, etc).
The hidden problem here is try/finally. It's a great language feature - indeed it's essential - but if a serious enough exception is flying up the stack, should it really be causing finally blocks to run? Do you really want the evidence to be destroyed when there's a bug in progress? And if the program is in an unknown state, anything important could be destroyed by those finally blocks.
So what you really want is:
try
{
}
catch (Non-fatal exceptions)
{
// log
}
catch (Fatal exceptions, without running finally blocks)
{
// shutdown
}
This feature doesn't exist in C#. You cannot examine an exception to see if it is "fatal" (by your own definition) without first running all open finally blocks in the stack between the throw and the catch.
However, it does exist in VB.NET and C++/CLI. There are many articles around the web about how to expose it for use from C#. The BCL itself does this - it's mostly written in C# but it uses VB to expose exception filtering.
Having done that, you then allow fatal exceptions to go unhandled. You instead enlist with the AppDomain.UnhandledException event, save as much information as you can for support purposes (at least the stack trace) and then call Environment.FailFast to shut down your process before finally blocks can execute.