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).
Yes, it is OK and it is a good way of hiding the details of your inner events as well as having one class to connect your event listeners to, instead of several one. It does not mean that having multiple events in multiple classes is bad - far from it - just that it helps in increasing the comprehension of your code if all events related to a particular business domain are in the same "place", although the real inner work is done in "child" classes.
There even is a name for it: event bubbling. You may be interested in reading the MSDN article on that subject.
Best Answer
Enh? It's not a great idea.
If you have a fixed set of ways the long running task could fail, then the enum may make sense. Since you say that there are a number of implementers for this interface, you likely have an open set of ways it can fail, making the exception more palatable.
In general, I dislike events for this sort of thing. You're not guaranteed that anyone is listening. Since something has already gone wrong, it's kinda likely that one of your event handlers will go off the rails, interrupting the chain and/or throwing an exception in the handler, leading to problems.
I would look to see if you couldn't change that to more of a single error handler delegate, perhaps using abstract base classes to force it to be provided via constructor rather than added post construction (which you can't really guarantee at compile time).
That said, passing around exceptions is a little smelly, but not terrible in cases like this. I mean, you're catching an exception and asking a strategy to deal with it. Converting the exception to something else (with likely loss of information) seems to be unnecessary overhead/confusion.