[Python-ideas] Consistent programming error handling idiom

Rian Hunter rian at thelig.ht
Fri Apr 8 12:40:15 EDT 2016


> From: Chris Angelico <rosuav at gmail.com>
>> On Fri, Apr 8, 2016 at 12:17 PM, Rian Hunter <rian at thelig.ht> wrote:
>> Contrast with the following code that never makes sense (and is why I
>> said that NameError definitely signifies a programming error):
>> 
>>   try:
>>       foo = bar
>>   except NameError:
>>       foo = 0
> 
> This is exactly the idiom used to cope with builtins that may or may
> not exist. If you want to support Python 2 as well as 3, you might use
> something like this:
> 
> try:
>   input = raw_input
> except NameError:
>   raw_input = input

Oops, good call. That was a bad example. Maybe this one is a bit better:

   try:
       assert no_bugs()
   except AssertionError:
       # bugs are okay
       pass

> This is what I'd call a boundary location. You have "outer" code and
> "inner" code. Any uncaught exception in the inner code should get
> logged rather than aborting the outer code. I'd spell it like this:
> 
> try:
>   response = ...
> except BaseException as e:
>   logging.exception(...)
>   response = create_500_response()
> 
> [snip]
> 
> At this kind of boundary, you basically catch-and-log *all*
> exceptions, handling them the same way. Doesn't matter whether it's
> ValueError, SyntaxError, NameError, RuntimeError, GeneratorStop, or
> SystemExit - they all get logged, the client gets a 500, and you go
> back and look for another request.

I'm very familiar with this pattern in Python and I've used it myself countless times. Unfortunately I've seen instances where it can lead to disastrous behavior, I think we all have. It seems to have limited usefulness in production and more usefulness in development. The point of my original email was precisely to put the legitimacy and usefulness of code like that into question.

> As to resetting stuff: I wouldn't bother; your functions should
> already not mess with global state. The only potential mess you should
> consider dealing with is a database rollback; and actually, my
> personal recommendation is to do that with a context manager inside
> the inner code, rather than a reset in the exception handler in the
> outer code.

So I agree this pattern works if you assume all code is exception-safe (context managers will clean up intermediate state) and there are no programming errors. There are lots of things that good code should do but as we all well know good code doesn't always do those things. My point is that except-log loops are dangerous and careless in the face of programming errors.

Programming errors are unavoidable. In a large system they are a daily fact of life. When there is a bug in production it's very dangerous to re-enter a code block that has demonstrated itself to be buggy, else you risk corrupting data. For example:

   def random_code(state):
       assert is_valid(state.data)

   def user_code_a(state):
       state.data = "bad data"
       # the following throws
       random_code(state)

   def user_code_b(state):
       state.db.write(state.data)

   def main_loop():
       state = State()
       loop = [user_code_a,
               user_code_b]
       for fn in loop:
           try:
               fn()
           except Exception:
               log_exception()

This code allows user_code_b() to execute and corrupt data even though random_code() was lucky enough to be called and detect bad state early on.

You may say the fix is to assert correct data before writing to the database and, yes, that would fix the problem for future executions in this instance. That's not the point, the point is that incorrect buggy code is running in production today and it's imperative to have multiple safeguards to limit its damage. For example, you wouldn't have an except-log loop in an airplane control system.

Sometimes an error is just an error but sometimes an error signifies the running system itself is in a bad state. It would be nice to distinguish between the two in a consistent way across all Python event loops. Halting on any escaped exception is inconvenient, but continuing after any escape exception is dangerous.

Rian


More information about the Python-ideas mailing list