I might invoke the wrath of Pythonistas (don't know as I don't use Python much) or programmers from other languages with this answer, but in my opinion most functions should not have a catch
block, ideally speaking. To show why, let me contrast this to manual error code propagation of the kind I had to do when working with Turbo C in the late 80s and early 90s.
So let's say we have a function to load an image or something like that in response to a user selecting an image file to load, and this is written in C and assembly:

I omitted some low-level functions but we can see that I've identified different categories of functions, color-coded, based on what responsibilities they have with respect to error-handling.
Point of Failure and Recovery
Now it was never hard to write the categories of functions I call the "possible point of failures" (the ones that throw
, i.e.) and the "error recovery and report" functions (the ones that catch
, i.e.).
Those functions were always trivial to write correctly before exception handling was available since a function that can run into an external failure, like failing to allocate memory, can just return a NULL
or 0
or -1
or set a global error code or something to this effect. And error recovery/reporting was always easy since once you worked your way down the call stack to a point where it made sense to recover and report failures, you just take the error code and/or message and report it to the user. And naturally a function at the leaf of this hierarchy which can never, ever fail no matter how it's changed in the future (Convert Pixel
) is dead simple to write correctly (at least with respect to error handling).
Error Propagation
However, the tedious functions prone to human error were the error propagators, the ones that didn't directly run into failure but called functions that could fail somewhere deeper in the hierarchy. At that point, Allocate Scanline
might have to handle a failure from malloc
and then return an error down to Convert Scanlines
, then Convert Scanlines
would have to check for that error and pass it down to Decompress Image
, then Decompress Image->Parse Image
, and Parse Image->Load Image
, and Load Image
to the user-end command where the error is finally reported.
This is where a lot of humans make mistakes since it only takes one error propagator to fail to check for and pass down the error for the entire hierarchy of functions to come toppling down when it comes to properly handling the error.
Further, if error codes are returned by functions, we pretty much lose the ability in, say, 90% of our codebase, to return values of interest on success since so many functions would have to reserve their return value for returning an error code on failure.
Reducing Human Error: Global Error Codes
So how can we reduce the possibility of human error? Here I might even invoke the wrath of some C programmers, but an immediate improvement in my opinion is to use global error codes, like OpenGL with glGetError
. This at least frees the functions to return meaningful values of interest on success. There are ways to make this thread-safe and efficient where the error code is localized to a thread.
There are also some cases where a function might run into an error but it's relatively harmless for it to keep going a little bit longer before it returns prematurely as a result of discovering a previous error. This allows for such a thing to happen without having to check for errors against 90% of function calls made in every single function, so it can still allow proper error handling without being so meticulous.
Reducing Human Error: Exception-Handling
However, the above solution still requires so many functions to deal with the control flow aspect of manual error propagation, even if it might have reduced the number of lines of manual if error happened, return error
type of code. It wouldn't eliminate it completely since there would still often need to be at least one place checking for an error and returning for almost every single error propagation function. So this is when exception-handling comes into the picture to save the day (sorta).
But the value of exception-handling here is to free the need for dealing with the control flow aspect of manual error propagation. That means its value is tied to the ability to avoid having to write a boatload of catch
blocks throughout your codebase. In the above diagram, the only place that should have to have a catch
block is the Load Image User Command
where the error is reported. Nothing else should ideally have to catch
anything because otherwise it's starting to get as tedious and as error-prone as error code handling.
So if you ask me, if you have a codebase that really benefits from exception-handling in an elegant way, it should have the minimum number of catch
blocks (by minimum I don't mean zero, but more like one for every unique high-end user operation that could fail, and possibly even fewer if all high-end user operations are invoked through a central command system).
Resource Cleanup
However, exception-handling only solves the need to avoid manually dealing with the control flow aspects of error propagation in exceptional paths separate from normal flows of execution. Often a function which serves as an error propagator, even if it does this automatically now with EH, might still acquire some resources it needs to destroy. For example, such a function might open a temporary file it needs to close before returning from the function no matter what, or lock a mutex it needs to unlock no matter what.
For this, I might invoke the wrath of a lot of programmers from all sorts of languages, but I think the C++ approach to this is ideal. The language introduces destructors which get invoked in a deterministic fashion the instant an object goes out of scope. Because of this, C++ code which, say, locks a mutex through a scoped mutex object with a destructor need not manually unlock it, since it will be automatically unlocked once the object goes out of scope no matter what happens (even if an exception is encountered). So there's really no need for well-written C++ code to ever have to deal with local resource cleanup.
In languages that lack destructors, they might need to use a finally
block to manually clean up local resources. That said, it still beats having to litter your code with manual error propagation provided you don't have to catch
exceptions all over the freaking place.
Reversing External Side Effects
This is the most difficult conceptual problem to solve. If any function, whether it's an error propagator or point of failure causes external side effects, then it needs to roll back or "undo" those side effects to return the system back into a state as though the operation never occurred, instead of a "half-valid" state where the operation halfway succeeded. I know of no languages that make this conceptual problem much easier except languages that simply reduce the need for most functions to cause external side effects in the first place, like functional languages which revolve around immutability and persistent data structures.
Here finally
is arguably the among the most elegant solutions out there to the problem in languages revolving around mutability and side effects, because often this type of logic is very specific to a particular function and doesn't map so well to the concept of "resource cleanup". And I recommend using finally
liberally in these cases to make sure your function reverses side effects in languages that support it, regardless of whether or not you need a catch
block (and again, if you ask me, well-written code should have the minimum number of catch
blocks, and all catch
blocks should be in places where it makes the most sense as with the diagram above in Load Image User Command
).
Dream Language
However, IMO finally
is close to ideal for side effect reversal but not quite. We need to introduce one boolean
variable to effectively roll back side effects in the case of a premature exit (from a thrown exception or otherwise), like so:
bool finished = false;
try
{
// Cause external side effects.
...
// Indicate that all the external side effects were
// made successfully.
finished = true;
}
finally
{
// If the function prematurely exited before finishing
// causing all of its side effects, whether as a result of
// an early 'return' statement or an exception, undo the
// side effects.
if (!finished)
{
// Undo side effects.
...
}
}
If I could ever design a language, my dream way of solving this problem would be like this to automate the above code:
transaction
{
// Cause external side effects.
...
}
rollback
{
// This block is only executed if the above 'transaction'
// block didn't reach its end, either as a result of a premature
// 'return' or an exception.
// Undo side effects.
...
}
... with destructors to automate cleanup of local resources, making it so we only need transaction
, rollback
, and catch
(though I might still want to add finally
for, say, working with C resources that don't clean themselves up). However, finally
with a boolean
variable is the closest thing to making this straightforward that I've found so far lacking my dream language. The second most straightforward solution I've found for this is scope guards in languages like C++ and D, but I always found scope guards a little bit awkward conceptually since it blurs the idea of "resource cleanup" and "side effect reversal". In my opinion those are very distinct ideas to be tackled in a different way.
My little pipe dream of a language would also revolve heavily around immutability and persistent data structures to make it much easier, though not required, to write efficient functions that don't have to deep copy massive data structures in their entirety even though the function causes no side effects.
Conclusion
So anyway, with my ramblings aside, I think your try/finally
code for closing the socket is fine and great considering that Python doesn't have the C++ equivalent of destructors, and I personally think you should use that liberally for places that need to reverse side effects and minimize the number of places where you have to catch
to places where it makes the most sense.
try/catch
is not "the classical way to program." It's the classical C++ way to program, because C++ lacks a proper try/finally construct, which means you have to implement guaranteed reversible state changes using ugly hacks involving RAII. But decent OO languages don't have that problem, because they provide try/finally. It's used for a very different purpose than try/catch.