I keep it simple.
A library has a base exception type extended from std:::runtime_error (that's from C++ apply as appropriate to other languages). This exception takes a message string so we can log; every throw point has a unique message (usually with a unique ID).
That's about it.
Note 1: In the situations where somebody catching the exception can fix the exceptions and re-start the action. I will add derived exceptions for things that can be potentially uniquely be fixed at a remote location. But this is very very rare (Remember the catcher is unlikely to be close to the throw point thus fixing the problem is going to be hard (but everything is dependent on situation)).
Note 2: Sometimes the library is so simple it is not worth giving it its own exception and std::runtime_error will do. It is only important to have an exception if the ability to distinguish it from std::runtime_error can give the user enough information to do something with it.
Note 3: Within a class I usually prefer error codes (but these will never escape across the public API of my class).
Looking at your trade offs:
The trade-offs I see include:
More exception classes can allow very fine grain levels of error handling for API users (prone to user configuration or data errors, or files not being found)
Do more exceptions really give you finer grain control? The question becomes can the catching code really fix the error based on the exception. I am sure there are situations like that and in these cases you should have another exception. But all the exceptions you have listed above the only useful correction is to generate a big warning and stop the application.
More exception classes allows error specific information to be embedded in the exception, rather than just a string message or error code
This is great reason for using exceptions. But the information must be useful to the person who is caching it. Can they use the information to perform some corrective action? If the object is internal to your library and can not be used to influence any of the API then the information is useless. You need to be very specific that the information thrown has a useful value to the person that can catch it. The person catching it is usually outside your public API so tailor your information so that it can be used with things in your public API.
If all they can do is log the exception then it is best to just throw an error message rather than lots of data. As the catcher will usually build an error message with the data. If you build the error message then it will be consistent across all catchers, if you allow the catcher to build the error message you could get the same error reported differently depending on who is calling and catching.
Less exceptions, but embedding an error code that can be used as a lookup
You have to determine weather the error code can be used meaningfully. If it can then you should have its own exception. Otherwise your users now need to implement switch statements inside there catch (which defeats the whole point of having catch automatically handle stuff).
If it can't then why not use an error message in the exception (no need to split the code and the message it makes it a pain to look up).
Returning error codes and flags directly from functions (sometimes not possible from threads)
Returning error codes is great internally. It allows you to fix bugs there and then and you have to make sure you fix all error codes and account for them. But leaking them across your public API is a bad idea. The problem is that programmers often forget to check for error states (at least with an exception an unchecked error will force the application to quit an un-handled error will generally corrupt all your data).
Implemented an event or callback system upon error (avoids stack unwinding)
This method is often used in conjunction with other error handling mechanism (not as an alternative). Think of your windows program. A user initiates an action by selecting a menu item. This generates an action on the event queue. The event queue eventually assigns a thread to handle the action. The thread is supposed to handle the action and eventually return to the thread pool and await another task. Here an exception must be caught at the base by the thread tasked with the job. The result of catching the exception will usually result in an event being generated for the main loop which will eventually result in an error message being displayed to the user.
But unless you can continue in the face of the exception the stack is going to unwind (for the thread at least).
These rules are generally a good idea and thus should be followed.
But remember these are generic rules. They don't cover all situations. They cover the most common situations. If you have a specific situation and you can make the argument that your technique is better (and you should be able to write a comment in the code to articulate your argument for doing so) then do so (and then get it peer reviewed).
On the counter side of the argument.
I don't see your example above as a good situation for doing so. If the logging system is failing (presumably logging some other exception) then I probably do not want the application to continue. Exit and print the exception to the output so the user can see what happened.
Best Answer
A couple of points to consider:
Listeners should be well-behaved in as much that they protect their own code's execution from letting exceptions escape to the outside.
The event firer (?), that is the one firing the event, has (should have) no interest in whatever the listeners do with it. Its only job is to ensure that all listeners get to hear about the event. This means that it should protect against one of the listeners doing anything that would prevent other listeners from getting the event. So, yes, catch exceptions.
The event firer has no interest in catching exceptions other than to ensure that bad behaviour by one listener doesn't affect other listeners. So in this case "eating" the exception could be argued for. However, eating exceptions doesn't help anybody, so logging the exception with as much detail about the offending listener as possible is the way to go.
I wouldn't have the firer collect exceptions and finally raise one of its own accord. Quite simply because the firer did its job correctly and has no need to throw an exception. By logging the exceptions thrown by badly behaving listeners, the firer is already doing its part in aiding any debugging needed. (Logs should always be free of exceptions...).