[Mono-list] Exceptions and error codes.
Thong Nguyen
tum@veridicus.com
Thu, 20 Mar 2003 12:13:44 -0800
Hiya guys,
I recently read some recommendations on the GTK# mailing list regarding the use of exceptions.
The recommendation Microsoft have been giving is that exceptions should be avoided unless absolutely required. The best way to report errors should be done through return error codes. The main reason behind this is speed. Microsoft has had a bad track record when it comes to writing robust and secure applications and much of the reason behind this is because of the Microsoft mentality of "speed is best" (that's partly why they are plagued with so many buffer overflow exploits). Using return error codes under the (often false) assumption that you'll get a speed increase will mean that you may introduce subtle errors into your code.
Now, I'm not going to tell everyone how to write design or write *their* projects but I feel that some people aren't quite getting the full picture with regards to exceptions and their use in a modern OO language. I hope that people will spend the time to read this message and come away with perspective that they consider useful.
Lets consider Int32.Parse. The intended function of this method is to parse a string into an int. The natural choice for the return value would be the parsed int. This allows us to use the method like so:
--------------------------------------------------------------
x = Int32.Parse(s);
// or
SomeClass.Foo(Int32.Parse(s));
--------------------------------------------------------------
Now, how do we report when the string passed is badly formatted? Firstly, lets consider using exceptions. Using an "exception" enabled Int32.Parse would look like tihs:
--------------------------------------------------------------
try
{
x = Int32.Parse(s);
}
catch (ParseException e)
{
Cosole.WriteLine(e.Message);
}
// or
try
{
SomeClass.Foo(Int32.Parse(s));
}
catch (ParseException e)
{
}
--------------------------------------------------------------
Now, lets consider avoiding exceptions and instead replacing the return value with an error code and returning the real value through an "out" parameter. The new Int32.Parse would be used like so:
--------------------------------------------------------------
int e;
e = Int32.Parse(s, out x);
if (e > 0)
{
Console.WriteLine("Error: {0}", e);
}
// Can't aggregate Int32.Parse as an argument to SomeClass.Foo
--------------------------------------------------------------
There are some problems with using the error code approach. Some are clear, and some are more subtle.
1) Not having the result of the Int32.Parse in the return value prevents us from using Int32.Parse as an argument to another method.
2) Error codes don't supply the "rich" information that exceptions do.
3) Unlike exceptions, error codes don't force redirection of program flow when there is an error. A program that doesn't check for an error code could continue for several more statements until it crashes with a seemingly unrelated error. Exceptions will cause an immediate jump to the closet error handler for the exception. If no error handler is available the program will terminate rather than continue running with (almost always) invalid state.
4) Exceptions force programmers to mix program logic and error handling. Programmers need to surround every method with an if/else statement. Forgetting to surround every method with an if/else statement can lead to errors as described in point 3. With exceptions, one try/catch can be used to handle the exceptions of more than one statement.
Here's a simple example of what I mean:
Lets say you have the following block of code:
--------------------------------------------------------------
x = Int32.Parse(s);
Foo(x);
y = Int32.Parse(t);
Bar(y);
--------------------------------------------------------------
Every statement can't be executed unless the previous statement suceeded.
Using exceptions, you'd end up with code like this:
--------------------------------------------------------------
try
{
x = Int32.Parse(s);
Foo(x);
y = Int32.Parse(x.ToString() + "0");
Bar(y);
}
catch (ParseException e)
{
Console.WriteLine(e.Message);
}
--------------------------------------------------------------
As you can see, the code is almost identical. Exceptions allow programmers to "program along the path of success" and then handle expected errors centrally (at the end of the code block). Notice how using exceptions here is useful even though the stack unwind feature of exceptions isn't fully utilised.
Using error codes you'd get something like this:
--------------------------------------------------------------
e = Int32.Parse(s, out x);
if (e == 0)
{
e = Int32.Parse(x.ToString() + "0", out y);
if (e == 0)
{
Bar(y);
}
else
{
Console.WriteLine("Error {0}", e);
}
}
else
{
Console.WriteLine("Error {0}", e);
}
--------------------------------------------------------------
The code is *harder* to read and understand because error handling is intertwined within the code. Also, notice how the error handling code had to be duplicated (you could avoid this using goto...ugh...). With error codes you *MUST* *ALWAYS* check the return code *THERE AND THEN* otherwise you could end up using invalid data. If you don't check exceptions *THERE AND THEN* you know that the remaining statements won't get executed with incorrect data. Exceptions are clearer and safer.
An alternative to exceptions and return error codes that some people consider are to add a "Error" property to the object in question (e.g. Stream.Error). This approach introduces subtle race conditions when those objects are used in a multi-threaded application. Those race conditions can sometimes be avoided using synchronization -- but then that defeates one of the intended reasons for avoiding exceptions (speed).
Here's an example of a race condition that I still see occuring:
--------------------------------------------------------------
Spot the race condition:
if (File.Exists(fileName))
{
reader = File.Open(fileName);
}
else
{
Console.WriteLine("error");
}
The following code doesn't have the race condition:
try
{
reader = File.Open(fileName)
}
catch (FileNotFoundException)
{
Console.WriteLine("error");
}
--------------------------------------------------------------
Using "state" to store error conditions is (IMO) a very bad idea.
Exceptions can be much slower than using error codes, however, it should be noted that the path of error is usually the least walked path. Most of the time your code will not throw exceptions. When exceptions aren't thrown there is *very* little overhead. In fact, the overhead of using error codes can be *greater* than that of using exceptions because you have to surround a significant number of statements with an if/else. Unlike exceptions, the if/else always takes CPU time; even when there is no error.
Unless you are using exceptions to report return values you'll rarely encounter performance issues involving exceptions because they will occur rarely. You'll get far more milage optimising your algorithms. Remember, as a rough guide, 90% of your CPU time is spent in 10% of your code!. If most of your process time is spent throwing exceptions then I think you should seriously think about redesigning your application! I think this makes a lot of sense; and it makes me to wonder what Microsoft were thinking when they recommended return codes over exceptions because of *speed*.
Here's my own personal recommmendations regarding exceptions:
+ Since all .NET methods can't and don't return error codes there are cases where exceptions *must* be used, therefore, be consistant and always use exceptions to report errors except for the "hotspots" in your code which should be carefully examined and optimized *afterwards*.
+ Use return values for return values. Don't use them to report errors. Exceptions were invented so we could use 'return' the way it was intended.
+ Don't use exceptions to report return values. Use exceptions to report errors that prevent your method from completing successfully.
+ Don't use state (e.g. an error property) as the only way to report errors. You'll get into trouble when threading.
+ If you really *must* use error codes to improve speed, you should supply a version that throws exceptions so that people aren't *forced* to check your error code "there and then".
+ It can be quite useful to supply methods that complement each other. File.Exists and File.Open are a good example.
e.g. The method File.Exists returns a bool (that makes sense since the purpose of the method is to check whether a file exists or not) but File.Exists should *not* throw FileNotFoundException. The method File.Open should return a TextReader and *not* a bool because the purpose of the method is to open a file. File.Open *should* throw FileNotFoundException.
I hope this post has been useful to people. I have spent a lot of time thinking about this isssue and I believe it is important. If anyone has any questions regarding this topic, please email me personally (remember to remove the SPAM guards from my email address).
All the best,
^Tum