Conditional clauses on `except`

Motivation

I’ve been working with OSErrors a lot recently, and the ergonomics keep annoying me, since I have to write this kind of boilerplate over and over:

try:
    ...
except OSError as exc:
    if exc.errno == ENOSYS:
        # handle it
    else:
        raise

The Python 3 rework of the OSError hierarchy helps a lot for common errnos (e.g. you can do except FileExistsError instead of having to check exc.errno == EEXIST), but that only helps for the specific errnos that have built-in types.

Idea

What if we could instead write:

try:
    ...
except OSError as exc if exc.errno == ENOSYS:
    # handle it

Details

Given a try block like:

try:
    ...
except ETYPES1 as VAR1 if COND1:
    ...
except ETYPES2 as VAR2 if COND2:
    ...
...

The meaning is: if an exception is raised inside the try body, then we iterate through the except clauses from top to bottom. For each one:

  1. Check if the exception’s class is a subclass of any ETYPES. If not, continue onto the next except clause.
  2. Bind the exception object to VAR.
  3. [THE NEW PART] Execute COND. If it returns something false-y, then unbind VAR and continue onto the next except clause.
  4. We have a match! Execute the except clause body.
  5. Unbind VAR.
  6. Break out of the loop over except clauses.
4 Likes

Interestingly, Visual Basic supports this via the When expression on a Catch statement (https://docs.microsoft.com/en-us/dotnet/visual-basic/language-reference/statements/try-catch-finally-statement#syntax), but C# does not support it. I think this is the only CLR feature supported by VB but not C#.

There’s some interesting discussion here: https://devblogs.microsoft.com/dotnet/the-good-and-the-bad-of-exception-filters/

The arguments given don’t really apply to Python, though. First, Python doesn’t have a two-pass exception handling mechanism. Second, we like to have finally clauses executed when an exception terminates the program.

Well, I was thinking more about the filter part, but I agree there’s not much discussion about filter there.

My main point is: filtering exceptions with an expression has some history. It might be interesting to know why C# chose not to implement it.

What would the behavior be if the if clause throws an exception itself?

Some random (semi-relevent) references…

I found this proposal for Ruby that seems similar, but it did not catch any steam, which I found curious since this kind of syntax seems quite Ruby-esque to me :stuck_out_tongue: The main objection seems to be that this signals an anti-pattern and it’s better to refactor the exception to be more fine-grained, like how Python introduced FileNotFoundError to eliminate the super common if e.errno != ENOENT: raise pattern. It has a point.

The except ... if ... syntax also reminds me a lot of conditional pattern matching, especially when you use it to catch errors:

try {
  ...
} catch {
  case e: EXC if COND => ...
  case e: EXC => ...
  case _ => ...
}

I know there’s a school that discourages this since it could lead to confusing flows if used to liberally (what happens if COND throws is a common example), but IMO it’s not that bad.

The same as if an exception was raised in the error handler:

>>> try:
...     1/0
... except ZeroDivisionError as exc:
...     exc + 1
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
TypeError: unsupported operand type(s) for +: 'ZeroDivisionError' and 'int'
>>> exc
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'exc' is not defined

It was already discussed before.

It is possible and not too difficult to implement. But I do not know whether it should be implemented.

Currently the workaround is re-raising and possible nested try/except:

try:
    try:
        ...
    except OSError:
        if exc.errno == ENOSYS:
            # handle ENOSYS
        elif exc.errno == EBADF:
            # handle EBADF
        else:
            raise
except OSError:
        # handle general OS error

Unfortunately the outer except OSError: catches also exceptions raised in handlers for specific errno. So more robust code can be:

handled = False
try:
    try:
        ...
    except OSError:
        if exc.errno == ENOSYS:
            handled = True
            # handle ENOSYS
        elif exc.errno == EBADF:
            handled = True
            # handle EBADF
        else:
            raise
except OSError:
    if handled:
        raise
    # handle general OS error

I do not know how common is such construction, but it is occurred sometime.

Interestingly, Visual Basic supports this via the When expression on a Catch statement (https://docs.microsoft.com/en-us/dotnet/visual-basic/language-reference/statements/try-catch-finally-statement#syntax), but C# does not support it. I think this is the only CLR feature supported by VB but not C#.

At some point (I think it was C# 6.0) C# added “catch () when (…)” syntax - https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/try-catch which turns into the filter. I think they eventually needed it to implement some other features like expression trees which also expose filters.

Yeah. And in fact, it’s already possible for exceptions to be raised inside an except clause filter, so this wouldn’t be a new thing:

>>> try:
...     raise RuntimeError
... except RnutimeError:
...     pass
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
RuntimeError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
NameError: name 'RnutimeError' is not defined
1 Like