"Explain it like I'm 5" for exception throwing.
Exception-handling is probably best thought of as modeling a
transaction mindset. Forget about the low-level control flow aspect. There is a control flow aspect to it, but it's like we don't want to use
functions as an elaborate
goto for branching. There's a higher-level concept and mental model associated with that which is better to focus on.
The idea is that things either succeed as a whole or fail as a whole.
// Transaction site. Try things out.
catch (Exception e)
// Recovery site. We might report a message to the user
// and also reverse any side effects (things we tried that
// didn't succeed as a whole, like deleting a file that we
// only managed to write half the contents of).
// Post-transaction site. Do things here that you need to
// do after the transaction whether or not it succeeded or
// failed, like closing a file.
External Error vs. Programmer Error vs. Non-Error
Now, unless you're working in Python which is totally bizarre to me with how they go about exceptions ("leap before you look"), normally the process of throwing exceptions is best utilized for external errors.
By external errors, I mean things you cannot help and are outside of your control. For example, trying to open a file might fail due to some environmental condition in the operating system (maybe the hard disk is full). Your program can't really help that so much. Maybe the user interrupted and aborted the middle of a loading process -- you can't anticipate that. Maybe allocating memory failed. All of these errors are the result of errors occurring externally, outside of your code.
This is the most natural place from which to throw an exception.
Programmer errors may or may not be tackled through exceptions. In C++, it's often discouraged in favor of an assertion mindset to catch programmer mistakes like accessing an array out of bounds, but I think C# might encourage throwing here. So a programmer mistake may or may not involve throwing exceptions depending on the language.
Outside of Python which has this "leap before you look" philosophy, generally you shouldn't throw for non-errors. By a non-error, I mean trying to find a key in a map and failing to find it. That's not an error so much as just a very valid post-condition for a find function (it didn't find it, very ordinary thing).
When you throw an exception, you are reporting an error. The idea is completely decoupled from catching. Don't think about where an exception will be caught or how it will caught when you
throw. That would tend to couple your function to the transaction and the call stack, and reduce generality. Potentially you might reuse this function in various transactions, but it can fail the same way. Throwing is kind of a shortcut to avoid manually having to
return error; all the way down to the
catch site, but try not to look at it that way so much.
Throwing is about reporting an error. It's broadcasting it to whomever is interested.
Throwing is Expensive
If this conceptual model of avoiding the throw for non-errors (like failing to find an item) is confusing, it's worth noting that throwing an exception can be very expensive (depending on language/compiler). Some implementations have to kind of suspend threads and stop the entire world.
You might equate it to the cost of a disk access. I don't know how expensive it is exactly and it seems to vary wildly from one language/compiler to another (in Python I imagine it has to be cheap), all you might need to know is that it is expensive. And that's why we don't want to necessarily use it to just report back non-errors to the caller that a key isn't found in a dictionary, e.g., in languages where this kind of thing isn't idiomatic. It's not a general branching mechanism, it's for genuinely exceptional paths.
Rethrow vs. Throw New
With transactions, you also have sub-transactions. For example, let's say that in the process of loading an HTML file (
trying to do this in a transaction), and you come across an image tag specifying an image file.
Now you might start a sub-transaction to load an image, and perhaps we fail to load the image and encounter an exception. In that case, you might
catch it inside your nested image transaction.
// Load image file and add the image to the webpage.
catch (Exception ex)
// We ran into an error loading the image.
// Close image file if it's open
However, this is a sub-transaction. We've encountered an error loading the image but it's also an outer error in loading the HTML file. As a result, we don't want to simply catch the exception and return from the function. We need to throw again. Here you can do one of two things:
catch (Exception ex)
// Translate the exception into a more meaningful one
// for the user.
throw new ImageException(...); // can embed a message containing
// details about what happened.
Swallowing up an exception and translating it this way might sometimes be useful if you want to try to report the error in a less "raw" kind of way to the caller of your image loading function.
... or you can do this:
catch (Exception ex)
// Simply throw the same exception to the outer transaction.
... and simply pass the original exception back to the caller.
Which one you choose here is more of a design decision, whether you want to simply forward the exact same exception to the outer transaction that invoked the image loader or translate it into some kind of specific image loading exception.
It's up to you and will be based on how rich you want the error reporting to be. I personally prefer dealing with fewer exception types and catching more generally since most of the time I don't find much use for an exception type beyond just grabbing the message attached, but that may be my C++ side speaking where programmer errors aren't reported through exceptions (they don't point out mistakes, only those external input errors outside of our control that can occur even when our code is perfectly correct).
In cases where exceptions are used in a debugging context more than just a "report what happened to the user" kind of mindset, you might find it very helpful to know exactly what kind of exception was thrown, and catch more specifically when possible as well as throwing a wider range of exception types.