Larry Osterman's WebLog

Confessions of an Old Fogey
Blog - Title

Exceptions as repackaged error codes

Exceptions as repackaged error codes

Rate This
  • Comments 15
One of the comments on my philosopy of error codes post from last week indicated that all the problems I listed with error codes were solved by exceptions.

The thing that I think the writer missed is that CLR (and Java) exceptions serve two totally different design patterns w.r.t. error handling.

You see, CLR exceptions solve both the "how do I report an error" problem, AND the "what information should be contained in my error report" problem.  The first part of the solution has to do with the asynchronous nature of exceptions - any statement can potentially throw an exception, and a caller is expected to catch the exception.  The second part is about what information is carried along with the error information.

IMHO, the System.Exception object is just another kind of error code object - it's functionally equivalent to an HRESULT combined with the IErrorInfo interface.  It's job is to provide sufficient context to the caller that the caller can determine some kind of reasonable behavior based on the error.

In fact, you could almost consider an exception hierarchy based off of System.Exception as a modern implementation of an X.400/x.500 OM error structure (X.400/X.500 OM errors are complex nested structures that described the source of the error, suggested recovery modes, etc).

The interesting thing about x.400/x.500 error codes is that they were sufficiently complicated that they were almost completely unusable.  Most people who manipulated them took the highly complex data structure and mapped it to a simple error code and operated off of that error code.  Why?  Because it was simpler - checking against a particular error code number was far easier than parsing the OM_error structure.

The good news for the "System.Exception as an uber error code" is that it's relatively easy to determine what kind of error failed from the strong type information that the CLR provides, which means that the "deconstruct the rich information into a simpler version" pattern I just mentioned isn't likely to happen.

But you should never believe that "exceptions" somehow solve the "how do I return sufficient information to the caller of my API" problem - exceptions per se do not, even though an object hierarchy derived from System.Exception has the potential of solving it.  But the "throw an exception to handle your errors" design pattern doesn't.

As a trivial counter example, consider the following C++ class (I'm just typing this in, don't expect this to work):

class Win32Wrapper
{
   HANDLE Open(LPCWSTR FileName)
   {
        HANDLE fileHandle;
        fileHandle = CreateFile(FileName, xxxx);
        if (fileHandle == INVALID_HANDLE_VALUE)
        {
            throw (GetLastError());
        }

    };
   DWORD OpenError(LPCWSTR FileName, OUT HANDLE *FileHandle)
   {
        *FileHandle = CreateFile(FileName, xxxx);
        if (&FileHandle == INVALID_HANDLE_VALUE)
        {
            return GetLastError();
        }
        else
        {
            return NO_ERROR;
        }
    };
};

This rather poorly implemented class has two methods.  One of them uses exception handling for error propagation, the other returns the error code.  The thing is that as far as being able to determine corrective action, the two functions are totally equivalent.  Neither of them give the caller any information about what to do about the error.

So claiming that the "throw an exception as a way of reporting an error" paradigm somehow solves the "what should I do with this error" problem is naive.

  • The exception has the opportunity to collect more information and handle that information in a single place.
    This is only beneficial for errors that are not going to be handled - but I do think it is beneficial for those errors.

    Throwing an integer is just plain daft - it has absolutely no context information is going to be caught somewhere out of context.
    Also, in your example getting an invalid handle from CreateFile would not usually be classed as fatal - as a result it should be handled there and then and error codes are more efficient for that purpose.

    But if GetLastError returns something utterly unsurvivable, you may as well collect as much information as possible (including the error code, the file name, a stack trace, SID, thread ID, process ID, etc...) and throw it.
    Code elsewhere can then tidily handle this fatal problem by sticking it all in the event log and dying gracefully.

    The biggest problem with this approach is that the seriousness of an error is application specific - so exceptions should be avoided in utility classes and (small) libraries.
  • I believe there is no general solution for the "what should I do with this error" problem. It simply depends on the context in which a given function is called which is normally unknown to the function.

    If Open() fails in an interactive application you show an error message and ask the user to enter a new filename. If it fails in a service you are pretty much done.

    The function call says WHAT should be done. The code inside defines HOW it is done. But there is no way to tell WHY it is done. You just cannot see the big picture from inside.
  • Er.. can you throw an int in C++? Bizarre. This is more an example of "throw a stupid exception and no-one will know what to do with it" or "poorly mixed error handling schemes are bad"

    How about: (java-y psuedocode I know nothing about C++ exceptions)

    HANDLE Open(LPCWSTR FileName)
    {
    HANDLE fileHandle;
    fileHandle = CreateFile(FileName, xxxx);
    if (fileHandle == INVALID_HANDLE_VALUE)
    {
    throw (new IOException(LookUpErrorMessage(GetLastError())));
    }
    };
  • In most of the ActiveX components I've written I've included extensive error reporting. For example, if calling a method or setting a property on my image processing control causes an error, the following information is returned:

    ReturnCode -- a regular return code (file not found, access denied, bad parameter etc.)

    ErrorSource -- was the error from my code, or was it an error caused by a dependency? If the file couldn't be open or some manipulation failed, it's an internal error; if ZLIB32 couldn't unpack a file, it's an external error.

    ErrorDetails -- what file couldn't be found, which parameter was wrong, etc. If the error was from an external source, any additional information that's available gets put here (for example the return code from ZLIB32 when failing to unpack an image)

    ErrorText -- some descriptive text with possible solutions ("Unable to open file 'blah blah' -- invalid or unknown TIFF header. The file may be corrupt or be in an unsupported format.", "Could not run convolve matrix -- values must be between -1 and 1", "Could not scale image -- both dimensions must be greater than zero", etc.)

    It may have been overkill, but it meant that anyone using the control could find out exactly what went wrong, why it went wrong, whose fault it was, and what they could do about it. 99% of the time the error code is probably sufficient, but for more esoteric things the additional information is a boon. Things like running a convolve matrix on an image require passing a bunch of parameters, so returning "Invalid parameter" isn't much help. Reporting _which_ parameter was invalid and what was wrong with it makes life much simpler.

    Of course the best solution is to write completely bug-free code and have it run on a bug-free OS by a user who never makes a mistake. :)
  • I did say that this example was a silly example.

    It was intended to show that "exceptions" alone don't solve the error handling problem.

    That's because there are TWO error handling issues - one is "what happened and what should I do with it", and the other is "how do I know something happened".

    Btw, as for CreateFile failures being exceptions, they are in the CLR - opening a file throws a System.FileNotFoundException (I don't have MSDN to check that this is the right exception)
  • Ok, so we have changed Larry's poor use of throwing the value from GetLastError to throwing the string resulting from GetLastError.

    Um, wow... great job. First problem is as a consumer of the exception, I have little or no reliable way to take corrective action on the error. Since you have converted the error code to text, I now would have to inspect the text to see what is says to try and take proper action. Sound *good* but what if this program is running on a French build, now my string tests will start failing. Also, there is the case of error codes that don't result in strings. Now we are throwing empty or common error strings and losing error resolution.

    The conversion of the error code to a string only serves to map the problem from one form to another (with great loss of data). Not only has it not improved the functional aspects of error processing, but it has made it much more error prone. From a programming aspect, you could have just done:

    throw "oops";
  • Oops. You are indeed correct. I didn't actually lose all of the information, I threw an IOException with a message rather than just a string. A better way of doing things would be (in java, again my C++ is pretty rubbish):

    class ErrorConverter
    {
    static Exception convert(int code)
    {
    switch (code){
    case FILE_NOT_FOUND:
    return new FileNotFoundException(getLocalizableMessage(code));

    etc.

    And to use:

    throw(ErrorConverter.convert(GetLastError));

    Java (and I assume C#) provides a mechanism for providing localizable error messages in exceptions.
    That way you get a specific error type, the type hierarchy allows you to treat groups of exceptions the same (FileNotFoundException is an IOException), and you get a human readable error message.

    (The example above will give you a slightly off stacktrace but there are ways around that)
  • If you stand any chance of handling the error GetLastError is sufficient - you already know the context.
    Making every API call throw an exception if it doesn't succeed would absolutely kill performance (in C++) - even if the API call succeeded.
    Does .net use SEH for .net exceptions or do they have some more efficient way of handling both the exceptions and the stack unwind (obviously they have less to worry about thanks to GC, but they still have "using" and other things to sort out).

    In fact, GetLastError is great - there are only a few circumstances where it causes problems (the worst offender being APIs that change the error return from an API that they call).
    What isn't great are the many and varied different failure returns that API's have.
    I hate having to manually check the docs for every API to see what the failure return value is - even though once I know that my 'exceptional' code is the same.
  • There are two sides to "what should I do with this error": what the program does with the error and what the user/administrator/developer does with the error.

    I agree that exceptions don't give the program any more help than an error code, but information about what the program was doing and what, e.g. file, it was acting on can be invaluable to solving configuration problems.
  • Oh God I'm stupid. I really should have read the post properly. So basically what Larry is saying is just throwing an exception without proper types, messages etc. is a useless as returning an error code that the client doesn't know how to deal with?
    Sorry about my babbling everyone.
  • Asd: Almost. To be totally specific, I'm saying that there are two UNRELATED issues that people comingle. The first (exception handling) solves the "how do I report an error to an application" problem. The second (System.Exception) solves the "what information do I report to the user" problem.

    But it's critical to realize that these are two UNRELATED problems.
  • The difference though is that exceptions have the potential to provide arbitarily rich error information whereas simple error codes don't. Just because there's no way to force programmers to actually provide that richness doesn't invalidate the whole method anymore than programmers who always return E_OUTOFMEMORY for any error condition invalidates the benefits of HRESULTs.

    One could also argue that in this trivial example the damage has already been done because CreateFile is limited in it's ability to return rich error context.
  • Mike, you're still comingling two unrelated concepts. You're talking about exceptions as if they were "system.exception". They're not. Exceptions solve the "how do I RETURN error information to the caller" problem. They don't solve the "what information should I tell the caller about the failure".

    Error codes are a solution to the latter problem, as is the System.Exception class, as is the OM error code mentioned above. Exceptions as a paradigm don't solve the problem per se. You can write code that uses exceptions to report error that is just as bad as returning an error code - that's what the example above is intended to show.

    You need BOTH exceptions AND a rigid structure of what can be thrown before you get a viable replacement.
  • Larry,

    Doesn't the Exception type and/or the message solve the "what information do I tell the caller" problem?
  • Scott: Yes it does. But "exceptions" don't.

    You could just as easily have a System.Exception be the NULL/Non Null return value from a function and have the same effect.
Page 1 of 1 (15 items)