Harm Veenstra recently published an article on his site about using Try/Catch/Finally and/or Traps in your PowerShell scripts.
This got me thinking about exceptions in general. Throughout my 20+ years programming in various languages, most supporting exceptions in some way, others not, their utility has, in my use cases, been hit and miss. I use them in my PowerShell scripts/programs with some regularity, certainly in languages which their use is enforced, and often times not in languages where their use is optional. In this article, I want to talk a bit about exceptions at large: what they are, why they’re used, and consequences of their use. I also want to use various languages to demonstrate where exceptions came from and what they look like across the board. This isn’t meant for confusion, more for perspective. If you aren’t familiar with these languages, don’t worry. The examples are trivial and easily understood.
Error States
I’ll start with a simple foundation: a C function that returns an integer:
int add_numbers(int a, int b) {
return a + b;
}
This is a really obvious function with, importantly, an equally obvious context. To call this function, the caller provides two integers and the function will return to the caller an integer whose value is the sum of the argument values. The caller can either capture the result in a new integer at the call site or discard:
int sum = add_numbers(5, 5); // CAPTURE
add_numbers(5, 5); // DISCARD
Now, in what ways can this function produce bad results? It may not seem like it could given its objective, but there are some nuanced things that could introduce issues, notably:
- The caller provides an argument(s) of (a) type(s) inconsistent with the signature.
- The values provided, when used in the arithmetic expression, result in a data type overflow.
The first case will always be caught by the compiler, so that’s not really the problem. What about the second case? Here’s how we can test this in C++:
#include <iostream>
#include <limits>
int add_numbers(int a, int b) {
return a + b;
}
int main(int argc, char** argv) {
std::cout << add_numbers(INT_MAX, INT_MAX) << std::endl;
return 0;
}
INT_MAX is a macro that defines the largest possible number that an integer variable could hold. For 32-bit integers, that number is 2147483647. So, what we’re doing in the main function is adding INT_MAX to INT_MAX, which would cause an overflow. This behavior is, by definition, undefined as the results of vary between OS and compiler. Some will use a binary technique called two’s compliment to wrap the value to the nearest negative number, but others won’t; your mileage will vary. In the case of Windows with MSVC, adding these two values together will result in -2.
What we need to pay attention to here is the fact that the add_numbers function is just performing an arithmetic operation every time it’s called, regardless of the integer values given to it. What it gives back to the caller may not always be what we’re expecting unless we have a reasonable amount of certainty about what’s being delivered as arguments. So, even with a function as trivial and potentially useless as this, some care is required when using it.
While valid, this example is perhaps a bit too academic for a thought exercise. Let’s look at something more practical: opening a file and reading data from it. Here’s one way of doing this in C++ using the standard library:
#include <iostream>
#include <fstream>
#include <string>
int main() {
std::string line;
std::ifstream myfile("example.txt");
if(myfile.is_open()) {
while(std::getline(myfile, line)) {
std::cout << line << std::endl;
}
myfile.close();
} else {
std::cout << "Unable to open file" << std::endl;
}
return 0;
}
This block of code will open the text file "example.txt", located in the working directory of the compiled binary, and for each line of text in this file, print that line to the console. If the file can’t be opened, whatever the reason, a message "Unable to open file" will be printed and the program will exit.
Now, as before, let’s identify all the ways in which this bit of code can go wrong:
- The string literal
"example.txt"is written in a way that implies a relative path, meaning the working directory of the binary (the directory where the binary is launched from). If that file doesn’t exist there, the program will print the message"Unable to open file". The same would be true if the path were absolute. - If
"example.txt"exists but is somehow corrupted in a way that prevents it from being read byifstream.
As the code stands now, the error handling is quite primitive. Regardless of the failure, all we get is "Unable to open file". Well, why can’t we open the file? Does the file not exist where we know it should be? Is it there but corrupted? Did it win the lottery and GTFO of town? It would be helpful if we had some way of determining what the failure cases are and to be able to react specifically rather than generally.
Historically, return values from functions coupled with tertiary “error value” variables have been used to discriminate error states. A prime example of this can be found in the Linux Kernel Documentation. Looking at the pertinent snippet from the open syscall function:

open, along with it’s compatriots, will, on error, set the value of the errno variable to one of those outlined in the ERRORS section, then return the integer -1 to the caller; this paradigm of returning zero on success and anything else being an error state is borderline canon in the C space. The implication here is that the return value of the function is required to be captured at the call site and asserted. Depending on the result of that assertion, you then need to assert the value of errno to figure out exactly what happened and structure reactionary code accordingly. This pattern isn’t too different from how contemporary exception handling mechanisms work in spirit, but they’re a bit more robust for certain aspects. This behavior is replete throughout the Linux Kernel and various other C/C++ projects, so programmers in these languages are very comfortable with this quasi-formal method of error handling. However, this technique isn’t exactly object-oriented and can be a little obtuse from a simple reading of the code. Another example of this can be found in SDL, even as recent as version 3.
Admittedly, C++ isn’t a great language to continue further with because it doesn’t enforce exception handling like Java and C# do. So we’ll jump to Java for now then likely kick over to C# and/or PowerShell. We’ll come back to this whole optional enforcement mechanism later.
That said, let’s look at some Java code that opens a file and prints its contents to the console:
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class ReadFileScanner {
public static void main(String[] args) {
try {
File file = new File("example.txt");
Scanner scanner = new Scanner(file);
while(scanner.hasNextLine()) {
String line = scanner.nextLine();
System.out.println(line);
}
scanner.close();
} catch(FileNotFoundException e) {
e.printStackTrace();
}
}
}
What Are Exceptions?
Simply put, exceptions are expressions of quantifiable error states in a program. As we’ve seen from the previous examples, we have code that does certain things (adding two operands together, opening a file for reading) that can, under certain conditions, do some thing(s) that we didn’t intend for it to do for one reason or another, but we’re now very interested in these reasons. These reasons are exceptions.
A good way to conceptualize exception handling is to think about the phrase “In what cases can my script/program be in an exceptional state?” I don’t mean exceptional as in “Wow you wrote the greatest program ever!”, I mean it in “Wow, this thing is totally FUBAR.” Pay very close attention to the verbiage in that rhetorical question: “How can this program state turn into burnt toast given some context?” Context here is both the internal and external influences over the state of the script/program. Internal concerns would be things like arithmetic result checks, reference validations, etc. External would be things like does a file that your program needs exist where you told it that it does. Exceptions force you to think about the environment that your code operates in and how that environment holds sway over your program in one way or another, and that you accept responsibility for maintaining the integrity of that state regardless of how things change. I’m romanticizing this a bit, but the truth doesn’t change regardless of my lexical flourishes.
How To Exception
In virtually every language that supports them, exception handling manifests in your code as try/catch(/finally) blocks, which aims to describe the workflow succinctly. Some languages (looking angrily at you Python) like to take liberties and change keywords, but normal and sane languages all use the former. First, you place potentially exceptional code in a try block where code could throw an exception, follow that try block with at least one catch block and optionally a finally block (some languages don’t implement finally blocks, but Java, C#, and consequently PowerShell do).
Let’s look again at our Java file opening example above and dissect it to understand what’s going on:
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class ReadFileScanner {
public static void main(String[] args) {
try {
File file = new File("example.txt");
Scanner scanner = new Scanner(file);
while(scanner.hasNextLine()) {
String line = scanner.nextLine();
System.out.println(line);
}
scanner.close();
} catch(FileNotFoundException e) {
e.printStackTrace();
}
}
}
The very first line in our main method declares a try block. This should inform you that the contained code has the possibility to introduce problematic states into your program. There are actually quite a few explicit exceptions we could catch given the code here, but for the sake of simplicity, we’ll keep going with this.
Following the try block is a single catch block. This catch block is an explicit catch block because it’s looking specifically for a FileNotFoundException to be thrown. In this example, if the Scanner instance fails to open the file "example.txt" when it’s instantiated, a FileNotFoundException will be thrown and this catch block will execute the instructions contained within. Here, we print the stack trace of the program and exit because we can’t really do anything unless the program can see "example.txt" in its working directory.
Try blocks can have any number of catch blocks appended to them. This is useful if you’re looking to enumerate all potential exceptions for a try block, the goal being to react distinctly to each kind of exception:
import java.io.*;
import java.lang.Exception.*;
import java.util.Scanner;
public class ReadFileScanner {
public static void main(String[] args) {
try {
File file = new File("example.txt");
Scanner scanner = new Scanner(file);
while(scanner.hasNextLine()) {
String line = scanner.nextLine();
System.out.println(line);
}
scanner.close();
} catch(NullPointerException e) {
e.printStackTrace();
} catch(FileNotFoundException e) {
e.printStackTrace();
} catch(IllegalStateException e) {
e.printStackTrace();
} catch(NoSuchElementException e) {
e.printStackTrace();
}
}
}
This amended example explicitly defines all possible exceptions that could be thrown from the try block. Being a language that enforces exceptions, Java requires that programmers explicitly define what kinds of exceptions a function, be it a method or constructor, will throw in its signature, so it’s easy to figure this out. And for the sake of simplicity, all catch blocks here will do the same thing. In practice, you’d only account for exceptions that you know you want to do something about if they’re encountered.
Alternatively, you can use a generic catch block which will catch any exception thrown:
import java.io.*;
import java.lang.Exception;
import java.util.Scanner;
public class ReadFileScanner {
public static void main(String[] args) {
try {
File file = new File("example.txt");
Scanner scanner = new Scanner(file);
while(scanner.hasNextLine()) {
String line = scanner.nextLine();
System.out.println(line);
}
scanner.close();
} catch(Exception e) {
e.printStackTrace();
}
}
}
This pattern is useful if you know a group of statements has the potential to throw an exception but you don’t really care about the specifics. This is possible because of inheritance. Every specific exception type (NullPointerException, FileNotFoundException, etc.) inherits from Exception. Because of the rules of type ducking, a NullPointerException is an Exception, ergo we’ve satisfied the catch clause requirement.
If you wanted to get super fancy, you can combine both the specializations and generics like so:
import java.io.*;
import java.lang.Exception;
import java.lang.Exception.*;
import java.util.Scanner;
public class ReadFileScanner {
public static void main(String[] args) {
try {
File file = new File("example.txt");
Scanner scanner = new Scanner(file);
while(scanner.hasNextLine()) {
String line = scanner.nextLine();
System.out.println(line);
}
scanner.close();
} catch(NullPointerException e) {
e.printStackTrace();
} catch(FileNotFoundException e) {
e.printStackTrace();
} catch(Exception e) {
e.printStackTrace();
}
}
}
Using this technique, we will have code that reacts specifically to either NullPointerException and FileNotFoundException, and for any other kind of exception, it gets handled in the last catch block.
Because Java is a language where exception handling is strictly enforced, the compiler will notice if you attempt to make a function call that could throw an exception which isn’t contained in a try block. This behavior is true for all languages that enforce these semantics. The idea being that if you have the possibility for errant states, you should be cognizant of them and prepared to react.
Finally blocks are found in Java, C#, and PowerShell to name a few. Code within a finally block is guaranteed to be executed after a try block exits, even if a catch block is or isn’t executed. This kind of code is most commonly seen immediately inside main functions where a “logic manager” will continuously execute the program within a try block and a finally block is used for program cleanup in the event the program terminates prematurely (Ctrl+C terminations, crashes, etc.). Here’s an example of this in PowerShell from a game I wrote called PSPlatformer:
Function Start-PSPlatformer {
Try {
Clear-Host; Write-Host "`e[?25l"
$Script:TheTicker.Start()
While($Script:Running -EQ $true) {
$Script:FrameStart = $Script:TheTicker.ElapsedTicks
& $Script:TheGameStateTable[$Script:GlobalState]
Draw-Screen
$Script:FrameEnd = $Script:TheTicker.ElapsedTicks
$Script:FrameDelta = [Stopwatch]::GetElapsedTime($Script:FrameStart, $Script:FrameEnd)
$Script:CurrentFps = 1.0 / ($Script:FrameDelta.Ticks / [Stopwatch]::Frequency)
$Script:SleepTime = [Math]::Max(0, ($Script:TargetFrameTicks - $Script:FrameDelta.TotalMilliseconds))
[Thread]::Sleep([TimeSpan]::FromMilliseconds($Script:SleepTime))
}
} Finally {
Restore-InitialGameState
Clear-Host
}
}
The code in the finally block here is guaranteed to execute upon termination of the game program, and is critical because the game program is a module. Prior to this, if a player died, the module would have to be unloaded then reloaded to work again. The general takeaway from this is that finally blocks should be used as guaranteed safety mechanisms for resource cleanups, if necessary, concerning relevant try blocks. Again, not every language implements finally blocks, so keep that in mind.
This is really the most direct way to use exceptions in mature, normal programming languages:
- Enclose potentially exceptional code in a try block.
- Accompany that try block with at least one catch or, if supported, no more than one finally block.
That’s it. End of story.
Why Exceptions?
This, IMHO, is a bit of a difficult question to answer. If you’re comparing the two paradigms described here (return value/tertiary state variable vs. exceptions), I don’t feel like there’s any clear winner or loser. They both have value within their respective contexts. Some of the calculus involves the edicts of the language you’re using, some programmer/project preference. Each technique implies a bit of boilerplate response code at call sites, some more cumbersome than others, with varying attempts to alleviate this having mixed results. There’s also the participation fee code, which I feel exceptions win in this department, but methinks this falls on the personal preference axis. Regardless, the germ of the idea is we’re trying to improve our in-code error state responses to ensure continued operation and as clean an environment as we can make, and both approaches work well. If I were pressed, I’d say that exceptions are great if either the language enforces their use or, if optional, you can comfortably implement them due to their ability to contribute to self-documenting syntax at the cost of type bloat. If you’re using a language that doesn’t support them, then you just can’t use them. Unless you wanna go ham and build your own exception codebase, but that’s all on you!
Exceptions as a concept date back farther than I think most people may realize, but their modern implementations as seen in C++ and Java are, objectively speaking, about as canon as you’re going to get. However, understand that we’re dealing with implementations of ideas, not de-facto standards.
Language Differences (Because We Make Things Easier By Making Them Harder)
If you’re still morbidly curious, let’s continue to Grandma’s House.
But Mr. Not Gary! In your PowerShell example above, there’s only a finally block associated to the try block! Isn’t that illegal?
Nope.
But Mr. Not Gary! The above C++ examples don’t showcase using exception handling!
Yep.
But mIsTeR nOt GaRy! You mentioned that Python is stoopid and ugly! You don’t really mean that, do you?
Of course I do. Especially in the case of exception handling.
So of the languages we’ve dabbled with this far, let’s state their exception support:
| Language | Exception Support | Caveats |
|---|---|---|
| C | Hell No | It’s C |
| C++ | Dangerously Optional | Throws can throw anything, throws can occur in functions whose signature doesn’t explicitly declare what can be thrown, noexcept is weird, no finally block support, etc. |
| Java | Yes | Condensing multiple catch matches in a quasi-discriminated union feels goofy. |
| C# | Yes | None that I can think of. |
| PowerShell | Yes | All the rules of C# apply here, adds a concept called a Trap, which is essentially a script-level try/catch feature that can use Script Blocks. |
| Python | Unfortunately | The unholy quadra-fecta of try/except (not catch, because fuck you)/else/finally is totally gauche, and because we’re not done shitting on common sense, throw is replaced with raise (more on this later), exception chaining is aneurism inducing. |
C
Pegging the return value of a function to its state may not seem like a bad idea until you realize that (A) that return value is entirely dependent upon a call capture to be retained for any interested foreign parties, and even then you’re still subject to the laws of scoping, or (B) maybe you want to return something else from the function that isn’t an error code; yes you can technically use reference parameters but let’s not complicate things too much. I won’t even mention documenting the return code meanings against something like setting errno because it’s effectively the same thing. So unless you’re prepared to create a struct that’s basically a void pointer married to a return state, and return that from every function regardless of what it’s intended to do, you’re mapping non-zero integer values to (hopefully) documented error states. Remember too that some vendors (still) don’t document APIs because that’s how we protect our IP in the 21st century.
There’s just no real sophistication here. C deals with primitives, and most times abstractions of those primitives end up being lipstick on a pig, and there’s just nothing to be done about it.
C++
Exceptions are a really bizarre, quasi-religious topic in C++ land which, if you’re lucky, will only get you kicked in the genitals twice.
Generally speaking, C++ doesn’t enforce their usage, but doesn’t really do enough to make you feel like using them won’t cause you to feel super dirty about it. Concurrently, some sects will swear they induce too much overhead to be of any practical value (usually in the embedded space) and will either not use them or use specialized compilers that purposely don’t support them so if you’re stupid enough to write either throw or noexcept, the compiler will complain to you before another programmer does.
Because exceptions are optional, using them is first dependent on you including the exception header from the STL. Once you’ve done that, you can start using your usual try and catch blocks; C++ doesn’t support finally blocks. When you start writing your catch blocks, you may be wondering what the hell you’re supposed to be trying to catch? Funny thing is, C++ allows you to throw anything. That’s right: a primitive (char, int, bool, unsigned int, string literal, numeric literal, interpolated string, your life savings, etc.) or an ADT. There’s no governance about what constitutes a throwable and what doesn’t. So to answer your question about what you should try to catch, my best response here is RTFM for the functions you’re enclosing in a try block to figure out what, if anything, they throw. But that’s likely going to be a bigger problem than you may think.
Throw specifications for function signatures are completely optional, and they, like seemingly everything else in the language, change. Prior to C++17, you would add a throw() spec to the end of a signature to tell the compiler that this function was going to throw something. There were permutations to this definition:
throw()- Function doesn’t throw.throw(...)- Function can vomit whatever it wants.throw(T)- Function can throw something of type T only.
Then, they decided to get a bit more slim with it using noexcept:
noexceptornoexcept(true)- Function doesn’t throw up.noexcept(false)- Function can vomit what it wants.
Here’s the kicker: if you don’t specify anything in the function signature, C++ assumes the function “could throw”, which means you’ve got no hope outside of reading the code of the immediate and any child function calls to figure out what could be coming your way. And even with noexcept(false), you still don’t have a hope in hell of figuring it out. Because of this, unless you’re dealing with functions in the STL (which, if they throw exceptions, they’re usually well documented), your best defense here is just a generic catch block:
try {
some_function();
} catch (const std::exception& e) {
std::cout << "Caught a standard exception: " << e.what() << std::endl;
} catch (...) {
std::cout << "Caught a cold!" << std::endl;
}
Many modern C++ libraries still don’t rely on exceptions either and will instead use the tried-and-true C method of setting some tertiary global error variable and returning a non-zero value from a function. We’re likely never to see stability in this space for exceptions, so you’re usually best off to avoid them entirely. Not that you’ll be in a disadvantage from doing so, but if you’re working in a polyglot project, it’s something to be aware of.
And just so we’re clear, this doesn’t work:
int add_numbers(int a, int b) {
return a + b;
}
int main()
{
try {
add_numbers(5, 5);
} catch (int e) {
std::cout << "Caught an integer exception: " << e << std::endl;
}
return 0;
}
Did I forget to mention too that some compilers will still need the -fexception flag passed to force the use of exceptions?
Java
Java kind of set the gold standard for modern exception handling, so there’s not really much to comment on here that isn’t already obvious or hasn’t yet been said. C# and many languages since have borrowed from its implementation (because Sun rules), but there are a few little quips about it that I can mention.
One common complaint about Java, and not just in the exception space, is that it’s a truly boilerplate code heavy language. I think a lot of OOP languages trend this direction and ends up being a brain-drain for programmers, but Java really wants to be this. Exceptions are no… exception. Reference the above examples all in Java.
Java has a syntax that will let you consolidate multiple type matches for a catch in a single clause that looks like an OR’d field or some kind of pseudo Discriminated Union:
...
} catch(IOException|IndexOutOfBoundsException e) {
// EXCEPTIONS WUZ HERE
...
}
The idea with this being it would prevent you from needing multiple catch blocks for each exception type. My immediate concerns with this were always two-fold:
- This is goofy syntax. The pipe operator is usually used for binary arithmetic (OR) and that’s not really what’s going on here. It’s also used for masking multiple enumeration values in a conditional, but that’s an actual binary operation.
- Even though we have some assurances that we’re catching something that IS an Exception, what if one of our specializations has a member that’s exclusive to it that one of the other types in the match clause doesn’t have, and we need that member? We have no choice then but to, in the catch block, disambiguate the type of
eto get the member so we don’t cause an invalid access exception on a member that may not exist, effectively nullifying the consolidated match clause. Meaning the only truly safe code here is the typicale.printStackTrace()or any of the inherited members fromException.
Truth be told, I’ve rarely seen this in production Java code. You’re more likely to see walls of catch blocks than you are consolidated matches, even if several of the catch blocks route to the same results.
C#
Exceptions in C# are pretty damn similar to those found in Java, so there’s not much to comment on. AFAIK there’s no consolidated match clauses, so everything else applies.
PowerShell
Exceptions in PowerShell are identical to C# (PowerShell is a subset/dialect derivative of C#). As noted before, the only addition is that of Traps. They act effectively as script-wide exception handling blocks. You can define traps anywhere in a script you want, but they might make the most sense being written at the top of a script (I have strong opinions about PowerShell programming). You can have a generic trap which is like a generic catch block, or an explicit trap which is like an explicit catch block. If at any point in the execution of the script an exception occurs, the trap is triggered. They’re a pretty neat script-wise way of using exceptions.
Python
Hot take: Python sucks. Period. This will just be one reason why.
Python’s exception handling takes the shape of try/except/else/finally blocks, all pivoting on the raise keyword. For some inexplicable reason, Python wants to be different in ways that can only be categorized as juvenile. And while I share a general interest in playing loose with tried methods, some things just shouldn’t be messed with, and this is one of them.
Here’s a simple bit of Python to demonstrate:
try:
result = 10 / 0
except ZeroDivisionError:
print("You cannot divide by zero!")
else:
print("No errors occurred.")
finally:
print("This always runs for cleanup.")
My dislike for replacing the catch keyword with except is strictly a matter of taste, much the same way that using raise instead of throw is. It would’ve been one thing if these keywords had stalled in C++ and Java and weren’t propagated through to other high-level languages; I’m not sure I’d be as offended. Programmers have clearly agreed on, largely, that the use of the verb throw, and the axioms it implies, are the de-facto way we think about exceptions. More importantly, it just makes sense. In this way, raising an exception makes sense, just as throwing one does, but you have to do some not-so-obvious mental gymnastics to arrive at except being the so-called verb that you use to capture a raised exception.
Putting this point more simply, in the land of normal people, we catch a thrown exception. In Python land, we except a raised exception. Makes perfect sense.
If this design wasn’t already an indicator of stupidity, let’s discuss the else keyword here. What does it do? Well, if you have a series of except blocks and you want some code to execute if and only if none of the except blocks fires, the code in the else block will execute. This is different from the finally block which will run regardless of the execute/else blocks.
Here’s the thing though: depending on your philosophy about the correct amount of bubbling with exceptions (more on this later), this whole else block is totally useless. For example, in C#:
try {
int numerator = 10;
int denominator = 100;
int result = numerator / denominator;
} catch (DivideByZeroException ex) {
Console.WriteLine($"An error occurred: {ex.Message}");
} catch (Exception ex) {
Console.WriteLine($"A general exception occurred: {ex.Message}");
} finally {
Console.WriteLine("This cleanup code always runs.");
}
Console.WriteLine("This is some text that runs after the try block");
Any code immediately following the try/catch/finally blocks here will run if none of the exceptions were caught and no other exceptions occurred while handling the blocks (unhandled exceptions will cause a termination; if you add a throw; statement in any of the catch blocks, this will result in an unhandled exception which will cause the program to terminate). Want a more Python specific example? Here we go:
try:
result = 10 / 0
except ZeroDivisionError:
print("You cannot divide by zero!")
finally:
print("This always runs for cleanup.")
print("Code that runs after the try/except block.")
The point here is that the exception mechanism should be built enough that if there is a terminating exception, it should do what it needs to and terminate. Otherwise, code flow will continue beyond the try statement. By dint of this, the block is, for all intents and purposes, “successful” and operation proceeds. This isn’t rocket science. The use of else in this context is over-engineering with no discernable benefit. In other words, by what metrics do you evaluate a try/catch block where code flow not continuing beyond the scoping boundaries of the blocks isn’t itself suitable to determine the supposed “success” or “failure” of the statement?
Finally, what about this exception chaining thing-a-ma-jig?
In Java, you would occasionally see this pattern bullshit used called exception wrapping. This is a fancy term for a delusion (which should maybe be in the DSM) that lead programmers to think that tucking a caught exception into another Exception, then throwing this new parent Exception was a good idea:
try {
some_method();
} catch(IOException e) {
throw new NotGarysStupidException("This is fucked", e);
}
Imagine using this in multi-target selecting.
It’s hard to conceive of why you would want to do this other than a propensity to make things more difficult than they need be. These were the kinds of code snippets you’d see in internet forums back in the early 2000s from someone in their CS 101 course, pawning if off as a novel concept. Just because you can do something doesn’t mean you should.
Why would we do this? The usual justification is that it’s an exception that was caused from another exception, so we’re attempting to procedurally account for total causality. The problem?
- Where does it actually stop at? How many exceptions do we need to account for before we just say fuck it and go back to basics (see bubbling theory later)?
- If you’re going through all the effort to have a specialization of
Exception, all just to marry semantics with some adjacentExceptionspecialization, why bother with the composition? If it means that much to you and for some reason.printStackTrace()isn’t good enough, just start appending strings. It’s cheaper and you get all your custom information you wanted so badly. Better yet, if you really want to make people mad, make a singleton that aggregates all your lower-order exception data then spits it out at the end of all the insanity! MUWAHAHAHAHA!
Keeping with the lipstick on a pig analogy, Python’s exception chaining gives us a syntactic shortcut:
try:
# SOME CODE HERE
except Exception as e:
raise MyError("Summary of what happened") from e
Exception Chaining/Wrapping has always bothered me. From a very specific conceptual perspective, it makes sense until you start actually trying to use it, in much the same vein as TDD. It’s clunky, cumbersome, difficult to read, and I’d argue that most of the time the data you’re attempting to collate from lower-order exceptions either (A) isn’t always as helpful as you may think and (B) while may have some value isn’t worth the hassle and contortions needed to logically explain away exception linkages like these, especially when stack traces are usually always more valuable… depending on where you bubble at.
Bubbling Theory
I have no idea if this is an actual term used in the industry or not, but it’s the name I’ve given to a discrete thought process surrounding the execution of exceptions.
Every time I write a catch block, my first thought is “Who’s going to see this?”
Invariably, the audience for the exception is a crucial consideration. I don’t necessarily limit the criteria to other programmers. In fact, they’re likely the least important group. There are, however, some groups which seem always to come up:
- Users
- Should users of my scripts/programs be privy to exceptions? If so, which kinds?
- Immediate Call Site Code
- Given where the potentially exceptional code is located at, should adjacent code at the call site be privy to exceptions? If so, which ones?
- Tertiary Code Locations
- How far away should splash damage from an exceptional measurement at one call site be visible to?
- External Partners
- Should an exceptional case be subject to logging functionality so that it can be consumed outside the program?
These questions, when used to interrogate call stacks, imply the what I call the bubbling question. Practically put, if I’m dealing with code at any level lower than user-level, should exceptions in these areas bubble to the top to the user’s attention or not?
Looking at C# for example, there are some exceptions which seem to lean into a bubbling behavior intrinsically: FileLoadException, FileNotFoundException, DirectoryNotFoundException, DriveNotFoundException, etc. Others, however, seem to imply a bit of grey area where you may want the user to be privy but may not: InternalBufferOverflowException and IOException. Others still beg to be hidden from the user: NullReferenceException and ObjectDisposedException. Bubbling Theory concerns itself with discriminating the visibility of exceptions in relation to, first, the user and, second, everything else.
Exceptions that the user should see should be handled in a way that is inline with their expected user experience profile. Exceptions shouldn’t cause new or novel interfaces to emerge, be it a GUI or TUI. If the user needs to react to an exception, like a FileNotFoundException, they need to be able to correct the issue in a way that’s natural to the environment. As was stated earlier, the line that distinguishes a bubbled exception from a non-bubbled exception can be murky, but the deciding factor should be the question “Should the user be doing anything about this?” In other words, if the code encounters a FileNotFoundException, is the situation one where the user could do something about it in a way that doesn’t cause them any unnecessary grief? If so, bubble it. If not, don’t. When exceptions are bubbled up to the user, the information they provide should unarguably be of benefit to them, not the programmers. In other words, if you bubble an exception to a user and the message presented to them contains programming jargon in it, parts of a stack trace, or the passcode for your luggage, you need your programming license revoked ASAP. It’s already bad enough that you bubbled an exception to the user, let alone one that’s just completely useless to them. If you need data that a programmer would need, exfiltrate that data elsewhere so that a programmer can access it. Don’t leave it to the user to figure that out. For any exceptional case that doesn’t pass the “Should the user be doing anything about this?” test, it should NEVER EVER be bubbled to the user under any circumstances. Those should be handled internally as best as possible.
Conclusion
Well, that was a mouthful. I hope you had an exceptional time reading this article, and that your exception game improved by an exceptional margin.