Patterns

Optional error handling

In some cases it may be desirable to keep exception handling in a helper that returns None on error. In the past, the project used an optional error argument that defaulted to True that indicated that exceptions should be raised. Callers could pass error=False to instruct the function to return None instead.

With Python’s typing annotations, these routines must be annotated as returning an Optional value. While the @overload decorator allows us to associate return types with specific argument types and counts, there is no way to associate a return type with specific argument values, like error=False.

A function annotated as returning an Optional value affects the implied types of the variables used to assign the result. Every caller of such a routine would need to check the result against None in order to drop the Optional annotation from the type. Even when we know the function cannot return None when passed error=True.

The way we handle this is to have separate functions for each case so that callers which will never have a None value returned do not need to check it.

Here are a few examples:

Function raises its own exceptions

from typing import Optional

import gdb

def new_routine(val: gdb.Value) -> str:
    if some_condition:
        raise RuntimeError("something bad happened")

    return val.string()

def new_routine_safe(val: gdb.Value) -> Optional[str]:
    try:
        return new_routine(val)
    except RuntimeError:
        return None

Function calls functions that raise optional exceptions

from typing import Optional

import gdb

def some_existing_routine(val: gdb.Value, error: bool = True) -> Optional[str]:
    if some_condition:
        if error:
            raise RuntimeError("something bad happened")
        return None

    return val.string()

def new_routine(val: gdb.Value) -> str:
    print("do something")

    ret = some_existing_routine(val)

    # This is required to drop the Optional annotation
    if ret is None:
        raise RuntimeError("some_existing_routine can't return None")
    return ret

def new_routine_safe(val: gdb.Value) -> Optional[str]:
    return some_existing_routine(val, False)