Warning! C++ programmer coming in here with possibly-different ideas
of how exception-handling should be done trying to answer a question
which is certainly about another language!
Given this idea:
For example, imagine we have fetch resource, which performs HTTP
request and returns retrieved data. And instead of errors like
ServiceTemporaryUnavailable or RateLimitExceeded we would just raise a
RetryableError suggesting the consumer that it should just retry the
request and do not care about specific failure.
... one thing I would suggest is that you might be mixing up concerns of reporting an error with courses of action to respond to it in a way that might degrade the generality of your code or require a lot of "translation points" for exceptions.
For example, if I model a transaction involving loading a file, it might fail for a number of reasons. Perhaps loading the file involves loading a plugin which does not exist on the user's machine. Perhaps the file is simply corrupt and we encountered an error in parsing it.
No matter what happens, let's say the course of action is to report what happened to the user and prompt him about what he wants to do about it ("retry, load another file, cancel").
Thrower vs. Catcher
That course of action applies regardless of what kind of error we encountered in this case. It's not embedded into the general idea of a parsing error, it's not embedded into the general idea of failing to load a plugin. It's embedded into the idea of encountering such errors during the precise context of loading a file (the combination of loading a file and failing). So typically I see it, crudely speaking, as the
catcher's responsibility to determine the course of action in response to a thrown exception (ex: prompting the user with options), not the
Put another way, the sites that
throw exceptions typically lack this kind of contextual information, especially if the functions that throw are generally applicable. Even in a totally degeneralized context when they have this information, you end up cornering yourself in terms of recovery behavior by embedding it into the
throw site. The sites that
catch are the ones that generally have the most amount of information available to determine a course of action, and give you one central place to modify if that course of action should ever change for that given transaction.
When you start trying to throw exceptions no longer reporting what's wrong but trying to determine what to do, that might degrade the generality and flexibility of your code. A parsing error shouldn't always lead to this kind of prompt, it varies by the context in which such an exception is thrown (the transaction under which it was thrown).
The Blind Thrower
Just in general, a lot of the design of exception-handling often revolves around the idea of a blind thrower. It doesn't know how the exception is going to be caught, or where. The same applies for even older forms of error recovery using manual error propagation. Sites that encounter errors do not include a user course of action, they only embed the minimal information to report what kind of error was encountered.
Inverted Responsibilities and Generalizing the Catcher
On thinking about this more carefully, I was trying to imagine the kind of codebase where this might become a temptation. My imagination (possibly wrong) is that your team is still playing the role of the "consumer" here and implementing most of the calling code as well. Perhaps you have a lot of disparate transactions (a lot of
try blocks) that can all run into the same sets of errors, and all should, from a design perspective, lead to a uniform course of recovery action.
Taking into account the wise advice from
Lightness Races in Orbit's fine answer (which I think is really coming from an advanced library-oriented mindset), you might still be tempted to throw "what to do" exceptions, only closer to the transaction recovery site.
It might be possible to find an intermediary, common transaction-handling site out of this here which actually centralizes the "what to do" concerns but still within the context of catching.
This would only apply if you can design some kind of general function which all of these outer transactions use (ex: a function that inputs another function to call or an abstract transaction base class with overridable behavior modeling this intermediary transaction site that does the sophisticated catching).
Yet that one could be responsible for centralizing the user course of action in response to a variety of possible errors, and still within the context of catching rather than throwing. Simple example (Python-ish pseudocode, and I'm not an experienced Python developer in the slightest so there might be a more idiomatic way of going about this):
# do some uniformly-designed recovery stuff here
# do some other uniformly-designed recovery stuff here
[Hopefully with a better name than
general_catcher]. In this example, you can pass in a function containing what task to perform but still benefit from generalized/unified catch behavior for all the types of exceptions you're interested in, and continue to extend or modify the "what to do" part all you like from this central location and still within a
catch context where this is typically encouraged. Best of all, we can keep the throwing sites from concerning themselves with "what to do" (preserving the notion of the "blind thrower").
If you find none of these suggestions here helpful and there's a strong temptation to throw "what to do" exceptions anyway, mainly be aware that this is very anti-idiomatic at the very least, as well as potentially discouraging a generalized mindset.