Error Code Paradigms
At some point when I was reading the comments on the "Exceptions as repackaged error codes" post, I had an epiphany (it's reflected in the comments to that thread but I wanted to give it more visibility).
I'm sure it's just an indication of just how slow my mind is working these days, but I just realized that in all the "error code" vs. "exception" discussions that seem to go on interminably, there are two UNRELATED issues being discussed.
The first is about error semantics - what information do you hand to the caller about what failed. The second is about error propogation - how do you report the failure to the caller.
It's critical for any discussion about error handling to keep these two issues separate, because it's really easy to commingle them. And when you commingle them, you get confusion.
Consider the following example classes (cribbed in part from the previous post):
class Win32WrapperException{ // Returns a handle to the open file. If an error occurs, it throws an object derived from // System.Exception that describes the failure. HANDLE OpenException(LPCWSTR FileName) { HANDLE fileHandle; fileHandle = CreateFile(FileName, xxxx); if (fileHandle == INVALID_HANDLE_VALUE) { throw (System.Exception(String.Format("Error opening {0}: {1}", FileName, GetLastError()); }
}; // Returns a handle to the open file. If an error occurs, it throws the Win32 error code that describes the failure. HANDLE OpenError(LPCWSTR FileName) { HANDLE fileHandle; fileHandle = CreateFile(FileName, xxxx); if (fileHandle == INVALID_HANDLE_VALUE) { throw (GetLastError()); }
};};class Win32WrapperError{ // Returns either NULL if the file was successfully opened or an object derived from System.Exception on failure. System.Exception OpenException(LPCWSTR FileName, OUT HANDLE *FileHandle) { *FileHandle = CreateFile(FileName, xxxx); if (*FileHandle == INVALID_HANDLE_VALUE) { return new System.Exception(String.Format("Error opening {0}: {1}", FileName, GetLastError())); } else { return NULL; } }; // Returns either NO_ERROR if the file was successfully opened or a Win32 error code describing the failure. DWORD OpenError(LPCWSTR FileName, OUT HANDLE *FileHandle) { *FileHandle = CreateFile(FileName, xxxx); if (&FileHandle == INVALID_HANDLE_VALUE) { return GetLastError(); } else { return NO_ERROR; } };};
I fleshed out the example from yesterday and broke it into two classes to more clearly show what I'm talking about. I have two classes that perform the same operation. Win32WrapperException is an example of a class that solves the "How do I report a failure to the caller" problem by throwing exceptions. Win32WrapperError is an example that solves the "How do I report a failure to the caller" problem by returning an error code.
Within each class are two different methods, each of which solves the "What information do I return to the caller" problem - one returns a simple numeric error code, the other returns a structure that describes the error. I used System.Exception as the error structure, but it could have just as easily been an IErrorInfo class, or any one of a bazillion other ways of reporting errors to callers.
But looking at these examples, it's not clear which is better. If you believe that reporting errors by exceptions is better than reporting by error codes, is Win32WrapperException::OpenError better than Win32WrapperError::OpenException? Why?
If you believe that reporting errors by error codes is better, then is CWin32WrapperError::OpenError better than CWin32WrapperError::OpenException? Why?
When you look at the problem in this light (as two unrelated problems), it allows you to look at the "exceptions vs. error codes" debate in a rather different light. Many (most?) of the arguments that I've read in favor of exceptions as an error propagation mechanism concentrate on the additional information that the exception carries along with it. But those arguments ignore the fact that it's totally feasible (and in fact reasonable) to define an error code based system that provides the caller with exactly the same level of information that is provided by exception.
These two problems are equally important when dealing with errors. The mechanism for error propagation has critical ramifications for all aspects of engineering - choosing one form of error propagation over another can literally alter the fundamental design of a system.
And the error semantic mechanism provides critical information for diagnosability - both for developers and for customers. Everyone HATES seeing a message box with nothing but "Access Denied" and no additional context.
And yes, before people complain, I recognize that none of the common error code returning APIs today provide the same quality of error semantics that System.Exception does as first class information - the error return information is normally hidden in a relatively unsophisticated scalar value. I'm just saying that if you're going to enter into a discussion of error codes vs. exceptions, from a philosophical point of view, then you need to recognize that there are two related problems that are being discussed, and differentiate between these two.
In other words, are you advocating exceptions over error codes because you like how they solve the "what information do I return to the caller?" problem, or are you advocating them because you like how they solve the "how do I report errors?" problem?
Similarly, are you denigrating exceptions because you don't like their solution to the "how do I report errors?" problem and ignoring the "what information do I return to the caller?" problem?
Just some food for thought.
Comments
- Anonymous
June 02, 2005
In many ways I was unknowingly dancing around this very observation with my post about the difference between throwing the return code and throwing the return code's string representation. My view was from the consumer standpoint which was more about what was being reported and not how it was being reported. It wasn't until your post that I realized this myself. - Anonymous
June 02, 2005
With the new longhorn system being more .Net based hopefully we'll see more innerExceptions describing exactly what the real reason was 5 levels down in the API. - Anonymous
June 02, 2005
What you say is correct, but it's not the only separation of errors that should be considered.
There are two types of fault - those that are expected and those that aren't.
Expected errors will be handled (whether by exception or error code) close to the point of the call that failed.
Unexpected errors have to return the system to a stable state and then get ready to try again - how far back they have to go in order to do this depends on nature of the application and the fault (it could be a network server that drops this request and readies itself for the next, or it could be a Windows app that kills the process and allows the user to restart it).
The key thing IMHO is that it is not the OpenFile function that should be making the choice - it is the caller that should.
More specifically, a library should never make the choice. - Anonymous
June 02, 2005
I agree with much of what Yaytay said. My first experiance with exceptions was with MFC's CFile class that just loved to throw exceptions in the case of opening files. In my application, file open errors are expected. Thus trying to use the exception system made the software much harder to write than using just simple fopen. - Anonymous
June 02, 2005
Yaytay, IMHO, that's a valid point but unrelated to my discussion.
The problem (as you mention) is that it's impossible for a DLL (or other library) to know what the intentions of the caller are. All it can do is to specify its contract and push the problem up to the caller. - Anonymous
June 02, 2005
The comment has been removed - Anonymous
June 02, 2005
oefe: How do you get the stack traceback?
Exceptions don't "automatically" maintain a stack trace unless you have the debugger break when someone throws an exception?
How do you differentiate between the real failure and the false positives? Because if the system's using exceptions exclusively you're likely to have false positives (I know, I've been burned by this before).
I think you're once again confusing a property of System.Exception (stack backtraces generated at the point of object construction) and a property of exception handling as a propogation paradigm. - Anonymous
June 02, 2005
The comment has been removed - Anonymous
June 02, 2005
The comment has been removed - Anonymous
June 02, 2005
Good post, but I think it's a bit odd to say "I recognize that none of the common error code returning APIs today provide the same quality of error semantics that System.Exception does". You've essentially staked out a middle ground that exists only in theory. Nobody who advocates for return codes over exceptions offers this as an alternative.
I'm not denying it's a vast improvement over returning an int - it is - but there must be some reason you never see this in practice. Is there a practical downside that makes this less attractive than it seems? There's some sort of runtime penalty (in C, at least). - Anonymous
June 02, 2005
The comment has been removed - Anonymous
June 02, 2005
The comment has been removed - Anonymous
June 02, 2005
I think the point is propagation of exceptions, and the fact that if the developer doesn't catch them or handle them in some way, he'll see it anyway(at runtime :-) ??? ). The code doesn't continue executing.
Having a system with error code, no matter how descriptive, it's totaly up to the developer to investigate the error code for success or failure. In this case the code keeps executing and this can lead to some major bugs which may not show themselfs right away. - Anonymous
June 02, 2005
The comment has been removed - Anonymous
June 02, 2005
I have to agree with Andrei. Coming from a Delphi background, exceptions feel very natural to me.
It takes the same amount of work to differentiate between expected and unexpected exceptions - reading the docs or code - but at least you can be sure you'll be stopped right in the place where you failed. The RTL makes sure you do.
Not something you can say for error codes. - Anonymous
June 02, 2005
Renaud,
If you catch an exception in the same function that threw it, the "cost" of throwing the exception is very low (I believe most compilers will generate a jump instruction directly to the correct exception handler).
But, this isn't the typical/expected case.
If you catch the exception in a different function, the costs start to rise dramatically. The act of throwing the exception triggers a software interupt. The operating system handles the interupt and attempts to find a handler for the exception. I'm not incredibly certain of all of the operations involved, but I think it would be reasonable to assume that stack unwinding is required along with logic to determine if the handler being checked can actually handle the exception (the logic is whatever the compiler generates to do the type checking/comparisons needed by the catch statement).
I believe that you can also factor in the cost of registering and unregistering each handler at the beginning and end of each try/catch block -- this isn't a significant cost, but it is something added that error codes don't need to deal with.
It IS an expensive operation. Now the question is, when an error condition occurs, do you care about perf?
This is assuming that we're talking about C++ here...I believe the performance implications are less severe in many cases for managed code (though I haven't read much about how it works under the hood).
One of the biggest problems with using exceptions in C++ is the lack of any sort of "finally" block. You can sort of work around the limitation by creating classes designed to do some sort of cleanup when destroyed, but it isn't natural and has its own set of problems ... (the biggest being, IMO, that you can't control the order in which the objects are destroyed).
For code that I write, I tend to use error codes for "problems" that I write code to handle; ie: the user never knows or sees the problem.
For cases where an error must surface to a user, I typically use an exception instead of an error code + extended information.
The biggest problems with using some extra API to report additional error information (GetLastError, IErrorInfo, etc) is that the information you wish to retrieve may be unexpectedly trashed before you can get to it. A contrived example:
MyFunc(_bstr_t("hello world"));
In this case, we'll say that MyFunc fails and sets a last error of access denied. However, when I make a call to GetLastError(), I'll get ERROR_SUCCESS. Why? Because the destructor for the _bstr_t calls SysFreeString, which sets the last error to ERROR_SUCCESS when the string is successfully deallocated.
This is an exceptionally bad problem as your error information can be wiped as a side effect of the internal workings of other code you know nothing about, which makes life "interesting"...:) - Anonymous
June 03, 2005
Renaud,
Exceptions are always an order of magnitude more expensive than returning an error code (regardless of the complexity of the error code).
The thing about exception codes is that you need to walk the stack looking for an exception handler for the type of the object being thrown (or ...). That stack walk takes time (potentially tens of thousands of instructions on non x86 architectures). Returning an error code means putting a value in a register. - Anonymous
June 03, 2005
I doubt that you can hold the additional impact exceptions have over error codes as detrimental. The application would make a similar impact if it attempted to diagnose the same information - and have a much more difficult time doing it.
That said I almost always use exceptions - it seems cleaner to me, and I don't have to worry about someone above me failing to deal with it. If they fail to deal with an exception, it moves up the stack to the next level.
However, having to determine the possible list of exceptions and attempt to bring the application back under control - well, that's the classic argument. "An error code is used to return an expected failure; an exception is used to indicate the system has failed completely." I've seen several places where the use of both is recommended, and the only purpose of catching an exception is to clean up as best as possible. - Anonymous
June 03, 2005
"How do you get the stack traceback?
Exceptions don't "automatically" maintain a stack trace unless you have the debugger break when someone throws an exception?"
Well, for my applications, I have a default exception handler set up for the app domain that takes any exception that I've failed to anticipate that I'd need to handle, then formats and dumps the whole thing, stack trace and all - and if it's one of "my" exceptions rather than a system one, extra information regarding thread, assembly version, etc. - straight into my bug-tracking system via its associated web service.
I catch a lot of unanticipated issues that way, and it's certainly a lot easier to implement than putting something similar everywhere I might get an error code. - Anonymous
June 03, 2005
The comment has been removed - Anonymous
June 03, 2005
The comment has been removed - Anonymous
June 03, 2005
Two comments.
1. In the Error version, the return value is never what you actually called the function for; it's always a return code. This makes seeing where values change much harder; you have to scan within the function parameters as well as down the left side of the code. I tend to lean towards the opinion that "ref" and "out" parameters usually indicate badly written or broken code for this reason.
2. This is related to the first point: the Error version makes the C# "using" and VB.Net "With" constructs impossible, thus bloating code and reducing readability. - Anonymous
June 03, 2005
Eric,
For #1, That's the error return information you're talking about - not the propogation information. And exceptions are about propogation. Which once again proves my point - you're talking about exceptions but referring to error return information.
For #2, THAT'S about propogation. And you're right, using and with aren't as useful in an error code propogation model - Anonymous
June 03, 2005
No, I am talking about propogation. Exception propogation doesn't require you to restructure your code and move variable changes all over the place. Consider:
int myCalculatedValue = 0;
...
int result = CalculateMyValue(param1, param2, ref myCalculatedValue);
if (result != RESULT_SUCCESS)
{
...
}
vs.
int myCalculatedValue = 0;
...
myCalculatedValue = CalculateMyValue(param1, param2);
In the second example, it is blindingly obvious even if you don't have the source for CalculateMyValue that myCalculatedValue is being changed. In the first example, it's not. Not using exceptions to propogate the error information (whether you "throw new int(5)" or "throw new Exception(lotsofinformation)") helps to conceal where the value could be changed, which can cause problems IME. - Anonymous
June 03, 2005
Ok, point taken. In other words, returning errors forces the contract for an API to be more obscure, thus forcing you to understand the use of all the parameters to an API while using exceptions allows the contract to be more obvious? - Anonymous
June 03, 2005
Yes, that's what I mean. :) - Anonymous
June 03, 2005
The comment has been removed - Anonymous
June 03, 2005
The comment has been removed - Anonymous
June 04, 2005
The comment has been removed - Anonymous
June 08, 2005
I just got this in the system log:
> Generate Activation Context failed for
> [censored......].exe.Manifest.
> Reference error message: The operation
> completed successfully.
This is a very famous error message, having been laughed at for at least a decade prior to Visual Studio 2005 beta 2.
Now the question is, what is the exception version of this error? How do you throw a non-exception? How do you catch a non-exception? - Anonymous
June 28, 2005
Note that the throw keyword[1] takes an expression, not necessarily an object deriving from System.Expression.
You can throw anything; similarly you can catch anything.
AFAIK (not tested!)
try
{
int x = 5;
throw x;
}
catch (int y)
{
}
is entirely legal.
[1]http://msdn.microsoft.com/library/default.asp?url=/library/en-us/csref/html/vclrfthethrowstatement.asp - Anonymous
August 10, 2005
The comment has been removed - Anonymous
August 16, 2005
The comment has been removed