18

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?

Luc Touraille
  • 79,925
  • 15
  • 92
  • 137
Aidas Bendoraitis
  • 3,965
  • 1
  • 30
  • 45
  • As in gimel's link above, EAFP is always preferable to testing for types, therefore you will see little of `type(a) == type(b)` comparisons in well written Python (although they are quite common in C++). If the caller of sum used incompatible types, she deserves a TypeError exception. – msw Apr 29 '10 at 18:13

7 Answers7

9

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.

function 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.

BenMorel
  • 34,448
  • 50
  • 182
  • 322
DevinB
  • 8,231
  • 9
  • 44
  • 54
  • 1
    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 Apr 29 '10 at 18:16
  • 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. – DevinB Apr 29 '10 at 19:26
  • 1
    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. – DevinB Apr 29 '10 at 19:32
  • 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. – DevinB Apr 29 '10 at 19:33
  • @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. – DevinB Apr 29 '10 at 19:34
  • I don't know if it's still true, but in Python it used to be that exceptions were cheaper than condition checking, provided the exception path was rarely taken. – Donovan Baarda May 08 '23 at 01:16
9

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.

Jochen Ritzel
  • 104,512
  • 31
  • 200
  • 194
2

Actually, the problem of using exceptions lies in the business logic. If the situation is exception (i.e. should not happen at all), an exception can be used. However, if the situation is possible from the business logic point of view, when it should be handled by conditional checking, even if this condition looks much more complicated.

For example, here is the code that I met in the prepared statement, when the developer is setting parameter values (Java, not Python):

// Variant A
try {
  ps.setInt(1, enterprise.getSubRegion().getRegion().getCountry().getId());
} catch (Exception e) {
  ps.setNull(1, Types.INTEGER);
}

With conditional checking this would be written like this:

// Variant B
if (enterprise != null && enterprise.getSubRegion() != null
  && enterprise.getSubRegion().getRegion() != null
  && enterprise.getSubRegion().getRegion().getCountry() != null) {
  ps.setInt(1, enterprise.getSubRegion().getRegion().getCountry().getId()); 
} else {
  ps.setNull(1, Types.INTEGER);
}

Variant B seems much more complicated from the first sight, however, it is the correct one, since this situation is possible from the business point of view (the country may not be specified). Using exceptions will cause performance issues, and will lead to misunderstanding of code, since it will not be clear, is it acceptable for the country to be empty.

Variant B can be improved by using auxiliary functions in EnterpriseBean that will return the region and country immediately:

public RegionBean getRegion() {
  if (getSubRegion() != null) {  
    return getSubRegion().getRegion();
  } else {
    return null;
  }
}

public CountryBean getCountry() {
  if (getRegion() != null) {
    return getRegion().getCountry();
  } else {
    return null;
  }
}

This code uses something like chaining, and each get method seems simple enough and is using only one predecessor. Therefore variant B can be rewritten as follows:

// Variant C
if (enterprise != null && enterprise.getCountry() != null) {
  ps.setInt(1, enterprise.getCountry().getId());
} else {
  ps.setNull(1, Types.INTEGER);
}

Also, read this Joel article about why exceptions should not be overused. And an essay by Raymon Chen.

SPIRiT_1984
  • 2,717
  • 3
  • 29
  • 46
  • The Joel article is specifically about C++ and Java. Python is a different beast with much better exception support and conventions. – Donovan Baarda May 08 '23 at 01:18
2

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
  • 73,987
  • 14
  • 101
  • 130
  • 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 May 03 '10 at 09:04
  • 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 May 03 '10 at 16:10
2

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
  • 364,293
  • 75
  • 561
  • 662
1

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
  • 10,545
  • 5
  • 33
  • 39
0

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
  • 11,409
  • 3
  • 61
  • 89