As in many issues related to programming it all depends...
I find that one really should first try to define your API so exceptional cases can not happen in the first place.
Using Design By Contract can help in doing this. Here one would insert functions that throw an error or crash and indicate a programming error (not user error). (In some cases these checks are removed in release mode.)
Then keep you exceptions for generic failures that can not be avoided like, DB connection failed, optimistic transaction failed, disk write failed.
These exceptions should then typically not need to be caught until they reach the 'user'. And will result in the user to need to try again.
If the error is a user error like a typo in a name or something then deal with that directly in the application interface code itself. Since this is then a common error it would need to be handle with a user friendly error message potentially translated etc.
Application layering is also useful here. So lets take the example of transfering cash from one account an other account:
transferInternal( int account_id_1, int account_id_2, double amount )
{
// This is an internal function we require the client to provide us with
// valid arguments only. (No error handling done here.)
REQUIRE( accountExists( account_id_1 ) ); // Design by contract argument checks.
REQUIRE( accountExists( account_id_2 ) );
REQUIRE( balance( account_id_1 ) > amount );
... do the actual transfer work
}
string transfer( int account_id_1, int account_id_2, double amount )
{
DB.start(); // start transaction
string msg;
if ( !checkAccount( account_id_1, msg ) ) return msg; // common checking code used from multiple operations.
if ( !checkAccount( account_id_2, msg ) ) return msg;
if ( !checkBalance( account_id_1, amount ) ) return msg;
transferInternal( account_id_1, account_id_2, amount );
DB.commit(); // This could fail with an exception if someone else changed the balance while this transaction was active. (Very unlikely but possible)
return "cash transfer ok";
}