I'm of the opinion (and admittedly this is an opinionated answer that's based on the sort of particular needs I encounter in my domain, the tools and languages I use, etc, and I wouldn't be surprised to find many disagreeing), that we should throw
and catch
"as generally as possible". So I guess I'm in the camp that wants to minimize the number of exception types there are to throw and especially minimize the number to uniquely catch (though not for performance reasons; I don't see any practical concerns there).
Now I don't expect everyone to agree with this so I'll just focus on why I think this way given my particular needs and experiences and tools involved.
And "generally as possible" does not necessarily mean using one exception type/class for every possible exceptional runtime situation the code can encounter. There might be cases where we need more information than that in a catch
block which might practically call for defining a new exception type and some cases where we might have two or three catch
blocks for a corresponding try
.
Example: LocalizedException
For example, in my language (C++), std::exception
only contains a string method (called what
) to return a string describing the error. In some of the specific types of runtime exceptions encountered specifically in my particular software, it might be useful to define a LocalizedException
class. If we catch that type of exception of subtypes of it, perhaps the what
method is not overridden to return a string describing the error, but actually a string describing a key to use into our application-specific message table which associates that key to a descriptive string translated to the user's chosen language. That's still somewhat of a contrived example I tried to come up with since it's likely better to just override what
for that type to do the lookup on the fly instead of making it the catcher's responsibility.
But even with this contrived example, you can see that's still rather generalized although a tad less general than std::exception
. I'm defining a new type specifically for the purpose of communicating something just a tad more specific than std::exception
which still unifies all the possible exceptions that could be thrown specifically by code under our control, with catchers of that particular LocalizedException
being expected to do something a bit different with the information provided by it than the most generalized language exception.
C++ and Our Use of Exceptions
In our case the language choice might matter a bit. We don't use exceptions to detect programmer bugs whatsoever, like an array being accessed out of bounds. I can see legit cases in software to want to gracefully recover even in the presence of programmer mistakes, but such runtime checks are not necessarily practical when we're dealing with things as performance-critical as raytracers. So we reserve such checks to try to detect programmer mistakes through asserts
which are eliminated away in release builds with the goal of detecting those in our automated tests before we ship.
Our use of exceptions is strictly limited to external runtime errors which are not programmer mistakes, like the user's file being corrupt, with the recovery goal being to tell the user what went wrong that's outside the developer's control and recover gracefully so that they can keep using the application.
Another aspect of C++ that I think tends to change the kind of exception-handling you commonly see in it from some other languages is destructors. I've often seen examples in other languages where, say, someone locks a mutex in a try block with the sole goal is making sure it gets manually unlocked if any exception is thrown, so that might naturally call for code with a whole lot more try
and catch
blocks all over the place and possibly a greater need to catch more specific types of exceptions. With C++ the recommended way to tackle that problem is just make a scoped mutex which automatically unlocks itself immediately when it goes out of scope (regardless of whether it goes out of scope in an exceptional path or not).
User-End Needs
Probably the reason I favor this is because our user-end needs tend to be simple with respect to handling exceptions. Often it's a matter of the user requesting to execute a command like loading a specific file they choose from a file dialog.
In that case the command's job is to either execute successfully or display an error message to the user and roll back the application state (if it attempts any state changes -- these days I tend to refrain from making commands change application state and instead return new application state absent side effects to avoid the need to roll back transactions in exceptional paths). The error message is often sufficient just being like this:
Failed to load file: "some_user_file.dat". Error: {some more specific reason here}.
For that more specific part of the error message, we do invest effort into trying to make it informative and user-friendly when it's something we can anticipate under our immediate control (i.e., we're the ones that throw
the exception, not some third party). For example, we might be parsing a proprietary scene file format which is text-based and sometimes edited or generated by hand by power users. In that case it might be really helpful to them to display where the data is malformed, in which line of text in that original file, etc. So in that case we're the ones parsing and the ones throwing the parsing error, so we try to make it as informative as we can within reason, and might throw the analogical LocalizedException
above to translate the resulting message to the user's chosen language.
However, there are like a gazillion runtime exceptions that could thrown outside of our control, including things impossible for us to anticipate in advance, and that'll get to my final rationale for why I favor things this way.
Exceptions Can Be "Implementation Details"
In our cases, what exceptions a function can throw are sometimes "implementation details" of a function, as in something you generally shouldn't need to know about (or even be much concerned about) to use the function correctly.
Of course with well-documented third party libraries and APIs, they do often document what exceptions/error codes their functions can throw/return, and in those cases we do try to anticipate some of the more common ones users can encounter (ex: failing to connect to a server in the underlying socket API) and catch and rethrow the exception into an exception that contains a more information message for users (and possibly with more contextual information). But that doesn't necessarily require that we define a SocketException
type. Since our catcher's general purpose is to just display messages of what went wrong to users, it might suffice to just use the analogical LocalizedException
in those cases.
But there are polymorphic functions in our case where this is impossible to do, because a lot of our architecture is plugin-based with third parties (including third parties from the future) writing plugins which define and implement these functions. And who knows what sort of runtime errors they will encounter, and report back to our system? We're not mind readers, we don't have time machines. I don't know what sort of errors/exceptions by someone outside of our team writing a plugin 2 years from now in the future could report back to us. So we can't help but, in those cases, just take the associated error and message and just display it to the user with some uber generalized exception type we might throw when encountering an error status in the plugin.
So in our case it's rather often that what can be thrown is an "implementation detail" that is impossible to x-ray, because we don't know why, how, or even when that function will be implemented and by whom. And maybe for those reasons because it's rather common in our case for it to be impossible to anticipate what might be thrown in advance and combined with needs to ship that I've favored the idea of throwing and catching as generally as possible.
Anyway, that's just my whole take for this, and I don't expect everyone to agree since our needs and tools and so forth are all different from one another.