Don’t Take Exception

By | 2013-07-10

Exceptions were an enormous improvement for error handling in software. Previously we would have code like this:

int a() {
   if( !do_something() )
     return error_code_for_a;
   return success_for_a;
}

int b() {
   if( a() == error_code_for_a )
     return error_code_for_b;
   return success_for_b;
}

int c() {
   if( b() == error_code_for_b )
     return error_code_for_c;
   return success_for_c;
}

int main() {
   if( c() == error_code_for_c )
     printf("Error in c()\n");
}

There are a few problems:

  • In a large application, the generator of an exceptional case has no knowledge of how to handle that exception.
  • The error therefore has to propagate from where it is generated to where it is handled.
  • Each function has to have a way of returning an error code as well as its real result; right through the chain of calls. What happens if the valid range for the non-error return value covers every possible value for that data type? There is then no way to steal one value to indicate an error.

Exceptions solve these problems by giving a separate return path for exceptional values.

int a() {
   if( !do_something() )
     throw exception_in_a;
   return success_for_a;
}

int b() {
   a();
   return success_for_b;
}

int c() {
   b();
   return success_for_c;
}

int main() {
   try {
     c();
   } catch( exception_in_a &e ) {
     printf("Error in c()\n");
   }
}

Note that b() and c() have no ability to handle the exception, so they can simply ignore it – the exception, should it occur, propagates right through them transparently.

At first sight then exceptions solve our problems. Not so. As they come, we rely on all the information about the exception being known by the generator. That is often not the case. The classic example is a failure during read.

void readSomething( int fd ) {
  if( read(fd, globalBuffer, 10) < 0 )
    throw libc_error(errno);
}

void openThenRead( const string &fn ) {
  fd = open(fn);
  read(fd);
}

void getConfigFile() {
  openThenRead("configfile.txt");
}

int main() {
  try {
    getConfigFile();
  } catch( libc_error &e ) {
    cerr << "Error reading file, errno=" << e.errno() << endl;
  }
}

What use is that as an error message? We have been told what the error was, but we have no way of finding out what file generated the error. Imagine of course that there are many files that may be opened in the application.

Java, by custom, although not by necessity, deals with this problem by wrapping one exception in another.

void readSomething( int fd ) {
  if( read(fd, globalBuffer, 10) < 0 )
    throw libc_error(errno);
}

void openThenRead( const string &fn ) {
  fd = open(fn);
  try {
      read(fd);
  } catch( libc_error &e ) {
      throw open_and_read_error(fn, e);
  }
}

void getConfigFile() {
  openThenRead("configfile.txt");
}

int main() {
  try {
    getConfigFile();
  } catch( open_and_read_error &e ) {
    cerr << "Error reading file, " << e.filename() << endl;
    try {
      throw e.innerException();
    } catch( libc_error &e ) {
      cerr << " - errno " << e.errno() << endl;
    }
  }
}

I’ve laboured the point a little, but the idea should be clear; by wrapping one exception in another, all of the information about an exception can be gathered together.

The problem is that this still has a major fault: the thrower ends up throwing at a particular target. If we have to write handlers for every combination of wrapper, and wrapped, to multiple depths, we will very quickly have to write far more handlers than is practical. Boost offers a pleasant mechanism for tidying this up. I’ll express the idea using a simplified syntax here, but you can get the real API from the Boost documentation.

void readSomething( int fd ) {
  if( read(fd, globalBuffer, 10) < 0 )
    throw boost::exception() << error_info_errno(errno);
}

void openThenRead( const string &fn ) {
  fd = open(fn);
  try {
    read(fd);
  } catch( boost::exception &e ) {
    throw e << error_info_filename(fn);  
  }
}

void getConfigFile() {
  openThenRead("configfile.txt");
}

int main() {
  try {
    getConfigFile();
  } catch( boost::exception &e ) {
    if( e.has<filename_t>() )
        cerr << "Error reading file, " << e.get<filename_t>() << endl;
    if( e.has<errno_t>() )
        cerr << " - error number, " << e.get<errno_t>() << endl;
  }
}

The point to observe is that openThenRead() catches the most generic type of error, and adds its own information to it as it passes. That same principle applies at all levels; meaning that no one ever has to catch more than one exception type.

It’s not my favoured option to use boost, as convenient as it is, as for me it’s gotten too large and unstable (it gets updated a lot, and I’ve had some builds fail on upgrades). In a future article I’ll describe a simpler version of boost’s exception class.

Leave a Reply