Skip to main content

Software Problems - Exceptions

· 3 min read

Exceptions are a common feature in popular languages like Python and Java. They serve to alter program execution under "exceptional" circumstances. However, handling them is not always straightforward. The concerns revolve around the following:

  • When an invoked function throws an exception, how should it be handled?
    • Should you catch and handle it?
    • Or let it bubble up to the caller? Does the caller's caller then need to worry about the exception? (a recursive question)

The problem is exacerbated because you can't simply avoid exceptions. Language libray functions often throw them (consider file handling, conversion, etc.). Moreover, writing exceptions is sometimes necessary, especially when dealing with external user input where "weird" and "unacceptable" cases may arise frequently. Furthermore, once you've written code that throws exceptions, you're likely to invoke that code yourself, necessitating handling your own exceptions. Poorly written code in this regard leaves no one to blame but ourselves.

The article Vexing exceptions by Eric Lippert is an interesting read on this topic. It classifies exceptions into four categories and suggests ways to handle (or not handle) them: fatal, boneheaded, vexing, and exogenous. A quick summary is provided in this post by Stephen Cleary. I'll briefly discuss what I learned from it.

The vexing exception is particularly interesting. Consider these two C# function signatures for parsing a string into an integer:

public static int Parse(string s)

public static bool TryParse(string s, out int result)

Invoking the first function, Parse, usually necessitates exception handling, as it will throw an exception if the input string is not convertible. An alternative approach when dealing with such functions is to seek or implement a variant like TryParse, which doesn't throw exceptions. TryParse returns a success indicator and the operation result. In cases of exceptions, it returns a failure indicator and a default value.

Here's an example in Python:

# Original
def parse_int(s):
return int(s)

# Usage
try:
result = parse_int("123")
print(result)
except ValueError:
print("Invalid input")

Using the TryParse variant will eliminate the need for exception handling, but it will require an if-else block to manage the success/failure case.

# TryParse
def try_parse_int(s):
try:
return True, int(s)
except ValueError:
return False, None

# Usage
success, result = try_parse_int("123")
if success:
print(result)
else:
print("Invalid input")

In summary, I think of vexing exceptions as "errors that are reasonably likely to occur". If so,

  • Use a "Try" variant without exceptions if available.
  • Implement a "Try" variant without exceptions if possible.
  • If neither is feasible, catch and handle (or re-raise) the exception.

Lastly, "Exogenous" exceptions, the siblings of vexing exceptions, are those thrown by code that you cannot reasonably control. A typical example is file handling functions. It's impractical to ascertain if a file exists before accessing it. Therefore, using code that does file handling likely requires try-catch blocks for possible exceptions. This differs from "Fatal" exceptions, where there's little you can do, while with exogenous exceptions, such as a file not being found, you can handle the situation, perhaps by creating a new file.

Here's a quick flowchart to summarize the ways to handle exceptions:

Summary of exception handling