views:

175

answers:

6

When is exception handling more preferable than condition checking? There are many situations where I can choose using one or the other.

For example, this is a summing function which uses a custom exception:

# module mylibrary 
class WrongSummand(Exception):
    pass

def sum_(a, b):
    """ returns the sum of two summands of the same type """
    if type(a) != type(b):
        raise WrongSummand("given arguments are not of the same type")
    return a + b


# module application using mylibrary
from mylibrary import sum_, WrongSummand

try:
    print sum_("A", 5)
except WrongSummand:
    print "wrong arguments"

And this is the same function, which avoids using exceptions

# module mylibrary
def sum_(a, b):
    """ returns the sum of two summands if they are both of the same type """
    if type(a) == type(b):
        return a + b


# module application using mylibrary
from mylibrary import sum_

c = sum_("A", 5)
if c is not None:
    print c
else:
    print "wrong arguments"

I think that using conditions is always more readable and manageable. Or am I wrong? What are the proper cases for defining APIs which raise exceptions and why?

+1  A: 

You should throw exception when the parameter contains an unexpected value.

With your examples, I would recommend to throw exception when the two parameters are of different types.

To throw an exception is an elegant way to abort a service without cluttering up your code.

Espen
+4  A: 

Generally, you want to use condition checking for situations which are understandable, expected, and able to be handled. You would use exceptions for cases that are incoherent or unhandleable.

So, if you think of your "add" function. It should NEVER return null. That is not a coherent result for adding two things. In that case, there is an error in the arguments that were passed in and the function should not attempt to pretend that everything is okay. This is a perfect case to throw an exception.

You would want to use condition checking and return null if you are in a regular or normal execution case. For instance, IsEqual could be a good case to use conditions, and return false if one of your conditions fails. I.E.

fuction bool IsEqual(obj a, obj b)
{ 
   if(a is null) return false;
   if(b is null) return false;
   if(a.Type != b.Type) return false;

   bool result = false;
   //Do custom IsEqual comparison code
   return result;
}

In that scenario, you are returning false both for the exception cases AND the "objects are not equal case". This means that the consumer (calling party) cannot tell whether the comparison failed or the objects were simply not equal. If those cases need to be distinguished, then you should use exceptions instead of conditions.

Ultimately, you want to ask yourself whether the consumer will be able to specifically handle the failure case that you encountered. If your method/function cannot do what it needs to do then you probably want to throw an exception.

Your advice that things that are able to be handled are inappropriate for exceptions seems off: that's why we have *exception handling*. This is especially true in Python where exceptions are used tons, including for very-expected cases such as indicating the end of a iterable is reached in a loop.
Mike Graham
I meant "handled *within* the method as opposed to handled without". Certain things can be handled seamlessly "inside the black box" as it were. That is the example I provided in my "IsEqual" method, it handled the exception cases, because the user isn't necessarily concerned with the validity of the inputs, only with their equality. Exceptions should be thrown when the *current context* cannot or should not handle it.
I'm well aware that exception handling exists. The main difference here is WHERE. In both cases you are handling an "exception" in the sense that it is an unexpected situation, however, if you are using comparisons, you are hiding that from the user. They will not be notified in an "exception" sense, they will be notified with a "null" or "error" response code, which is *overloading* the return parameter. Overloading the return parameter is almost **always** a bad idea, because then you are forcing the calling party to check after they call your function.
This is fundamentally different from throwing an exception, because they can choose to handle that or not (they could just let the exception bubble up). When using exceptions, the user knows that it completed successfully without doing anything extra, when you handle it internally you are saying it completed successfully *always*. If you can always generate a valid response, then you should use comparisons.
@Mike Graham Sorry, but I haven't used python in many years, so I'm actually a bit rusty on it. Generally, exceptions add a performance tax, i don't know if this is true in python.
+5  A: 

Exceptions are much more manageable, because they define general families of things that can go wrong. In your example there is only one possible problem, so there is no advantage to using exceptions. But if you had another class that does division, then it needs to signal that you can't devide by zero. Simply returning None wouldn't work anymore.

On the other hand, exceptions can be subclassed and you can catch specific exceptions, depending on how much you care about the underlying problem. For example, you could have a DoesntCompute base exception and subclasses like InvalidType and InvalidArgument. If you just want a result, you can wrap all computations in a block that catches DoesntCompute, but you can still do very specific error handling just as easy.

THC4k
+1  A: 

If you're asking, you should probably be using exceptions. Exceptions are used to indicate exceptional circumstances, a specific case where things work differently from other cases. This is the case for prettymuch all errors and for many other things as well.

In your second implementation of sum_, the user has to check every single time what the value was. This is reminiscent of the C/Fortran/other-languages boilerplate (and frequent source of errors) where error codes go unchecked that we avoid. You have to write code like this at all levels to be able to propagate errors. It gets messy and is especially avoided in Python.

A couple other notes:

  • You often don't need to make your own exceptions. For many cases, the builtin exceptions like ValueError and TypeError are appropriate.
  • When I do create a new exception, which is pretty useful, I often try to subclass something more specific than Exception. The built-in exception hierarchy is here.
  • I would never implement a function like sum_, since typechecking makes your code less flexible, maintainable, and idiomatic.

    I would simply write the function

    def sum_(a, b):
        return a + b
    

    which would work if the objects were compatible and if not it would already throw an exception, the TypeError that everyone is used to seeing. Consider how my implementation works

    >>> sum_(1, 4)
    5
    >>> sum_(4.5, 5.0)
    9.5
    >>> sum_([1, 2], [3, 4])
    [1, 2, 3, 4]
    >>> sum_(3.5, 5) # This operation makes perfect sense, but would fail for you
    8.5
    >>> sum_("cat", 7) # This makes no sense and already is an error.
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 1, in sum_
    TypeError: cannot concatenate 'str' and 'int' objects
    

    My code was shorter and simpler yet is more robust and flexible than yours. This is why we avoid typechecking in Python.

Mike Graham
The sum_ function I used was just a dummy example to illustrate the two possible ways of handling results. Normally I wouldn't have such a function.
Aidas Bendoraitis
I understand it was just a silly example, but I was noting that typechecking is a common type of condition checking that people think they should do, but isn't something you do much in good Python code.
Mike Graham
A: 

My main reason for preferring exceptions to status returns has to do with considering what happens if the programmer forgets to do his job. With exceptions, you might overlook catching an exception. In that case, your system will visibly fail, and you'll have a chance to consider where to add a catch. With status returns, if you forget to check the return, it will be silently ignore, and your code will continue on, possibly failing later in a mysterious way. I prefer the visible failure to the invisible one.

There are other reasons, which I've explained here: Exceptions vs. Status Returns.

Ned Batchelder
A: 

Maybe sum_ looks fine alone. What if, you know, it actually is used?

#foo.py
def sum_(a, b):
    if type(a) == type(b):
        return a + b

#egg.py
from foo import sum_:
def egg(c = 5):
  return sum_(3, c)

#bar.py
from egg import egg
def bar():
  return len(egg("2"))
if __name__ == "__main__":
  print bar()

If you ran bar.py you would get:

Traceback (most recent call last):
  File "bar.py", line 6, in <module>
    print bar()
  File "bar.py", line 4, in bar
    return len(egg("2"))
TypeError: object of type 'NoneType' has no len()

See -- usually one calls a function with the intent to act on its output. If you simply "swallow" the exception and return a dummy value, who uses your code will have an hard time troubleshooting. First off, the traceback is completely useless. This alone should be enough reason.

Who wants to fix this bug would have to first doublecheck bar.py, then analize egg.py trying to figure out where exactly the None came from. After reading egg.py they'll have to read sum_.py and hopefully notice the implicit return of None; only then they understand the problem: they failed the type check because of the parameter egg.py put in for them.

Put a bit of actual complexity in this and thing get ugly really fast.

Python, unlike C, is written with the Easier to Ask Forgiveness than Permission principle in mind: if something goes wrong, I'll get an exception. If you pass me a None where I expect an actual value, things will break, the exception will happen far away from the line actually causing it and people will curse in your general direction in twenty different languages, then change the code to throw a suitable exception (TypeError("incompatible operand type")).

badp