Tuesday, August 4, 2009

Exceptions Versus Return Values

I'm alarmed by the number of programmers I've been talking to lately -- not just C/C++ programmers, but also Java and C# programmers -- who are inclined to forgo the throwing of exceptions and instead use magic return values and/or status codes to indicate an API failure.

Consequenty I sat down to write a blog post about this. As a general rule, before I write something, I check to see if someone else has done it already, and sure enough, a number of people have taken on this issue competently. So instead of imitating their work, I wanted to point you to the best example, namely Ned Batchelder's.

One slight addition I wanted to make was to point out that Ned's article is primarily focused on the use of return values as status codes. I'm actually more concerned about overloading the return value with a sentinel value that is intended to indicate failure (a practice that Ned covers in the section on "Valuable channels"). In my opinion, this is far more insidious than the correct application of the status code idiom because it can time- and space-shift errors far from where they originate, which can unnecessarily complicate debugging.

Take the following square-root function as an example. Let's use C++.
float mysqrt(float n)
{
  if( n < 0 )
  {
    return -1.0;
  }
  return sqrt(n);
}
Here, -1 is being used as a sentinel value to indicate what any high school math student knows, namely, that you can't take the square root of a negative number. Consider what a client of this function must go through. Say I'm starting with the area of a circle, and I want to calculate the circumference using a couple of convenient helper methods:
float radiusFromArea(float area)
{
  return mysqrt(area / 3.14);
}

float circumferenceFromRadius(float radius)
{
  return 2 * 3.14 * radius;
}
Here is the client code:
float area = TEST_AREA;
float radius = radiusFromArea(area);
float circumference = circumferenceFromRadius(radius);
cout << circumference << endl;
If TEST_AREA is negative, we notice that the circumference is whacked (-6.28) when we are all done. That is, hopefully we notice and don't propagate the bad value forward, but I'll give the author of this code the benefit of the doubt for now. Even so, we still have no idea if the error is in radiusFromArea, circumferenceFromRadius, or some unknown function that one of them calls. Now condider what happens if mysqrt is written to throw an exception rather than return a "magic" error value:
float mysqrt(float n)
{
  if( n < 0 )
  {
    throw invalid_argument("can't take sqrt of negative number");
  }
  return sqrt(n);
}
When this code runs on a negative area for input, you see the error immediately in the form of an unhandled exception, with a stack trace pointing right to where mysqrt is called with the bogus value. This is much preferable for debugging, even in this simplistic example. Hopefully this will help convince you that exceptions are superior to the alternatives.

No comments: