melbalabs journal

practical mysticism

27 Jul 2020

common branching errors

I often see people forgetting to handle all possible branching scenarios properly. It comes up in all kinds of shapes in different programming languages.

The two common errors I see are

  • non-exhaustive branching
  • unchecked assumptions in a branch

The two problems are closely related.

non-exhaustive branching

The basic idea is that you forget to handle a special case, which often results in undefined and unwanted behavior at runtime. For example assume msg can have one of the values val1 val2. The obvious buggy code would be

if msg == 'val1':
    something
else if msg == 'val2':
    another thing
rest of code

Why is the code buggy? Because there's no catch-all else to handle

  • the distant future when msg is allowed to have val3
  • when someone fat-fingers the wrong value
  • when unexpected input arrives from some other system

Less obvious examples usually involve multiple variables.

The problem is every programming language tries to reinvent some syntax and has multiple ways to express control logic, so your pattern recognition might not save you. You have to actively think about the possibility of this bug.

Eg with python exceptions where the programmer was lazy and explicitly disabled the catch-all case from erroring out while he was testing.

try:
    code
except RuntimeError as e:
    save me
except Exception as e:
    pass

Eg in bash

case $msg in
val1)
    something
    ;;
val2)
    another thing
    ;;
*)
    catch all you shall not forget
    print error
    exit 1
    ;;

Java might try to save you by erroring out when you switch on enums and you forget to add the default/return/throw statement in the end.

enum MSG {val1, val2};
...
switch(msg)
{
case val1:
    return 1;
case val2:
    return 2;
default:
    do not forget me
}

Haskell has optional warnings on incomplete patterns with -fwarn-incomplete-patterns but it can be off.

fn " " = " "
fn "" = "did you forget me, the empty list?"

unchecked assumptions

As a project evolves, assumptions that held in the past, no longer do.

if msg == 'val1':
    something
else:  # msg can only be 'val2'
    another thing
    but that assumption is not checked, so who knows if it's still correct?

msg now allows a third value, so you're in trouble.

When you don't know what to do with the future, it's probably a good idea to throw an error. Better than overwriting data silently, because you were hoping something else will crash.

if msg == 'val1':
    something
else if msg == 'val2':
    another thing
else:
    raise RuntimeError("unknown msg {}".format(msg))

Here's how to be safe with exceptions.

try:
    code
except KnownError as e:
    I know how to thandle this error
except Exception as e:
    logger.warning('something is wrong, so better log it ASAP')
    # reraise the same exception
    # someone might know what to do with it
    raise