In an earlier blog entry, I expressed my liking for Andrei Alexandresciu's presentation about the D programming language:
D tries to make the easiest code to write also correct. Handle errors. E.g. throwing exceptions, implicitly caught outside main, usual path for library error reporting.http://blog.andy.glew.ca/2009/07/andrei-alexandrescu-case-for-d-attended.html
I.e. it made me look more favorably than before on exceptions.
Moreover, I have long found that "stacked exceptions" are one of the best ways of providing useful error messages: throw an exception, with a string message explaining the context If caught and handled by the caller, great. If not caught and handled by the caller, but caught somewhere higher up, then that place can either handle it - or can concatenate more context information. And so on.
So, the exception either gets handled... or you get a useful set of nested, stacked, context error mssages:
- error: file Foo.tmp could not be created, already existed, not overwiteable.
- error: Unpacking archive Foo.tgz
- error: installing software package Foo
I.e. not a useless error message like "could not open file of unrecognizable name". Not a segmentation fault. And not a buffer overflow.
--
However, what Rin Jeffries wrote in the [XP] mailing list thread "Exceptions are evil?" also appeals:
** Exceptions are not an object oriented mechanism
This is true, they are not OO, although one can implement an exception object.
** In reality, all exceptions are is a mechanism for a multi-level return.
** A method may return its intended value, or an exception.
That's a bit like saying "all a nuclear weapon is is a big firecracker".
It may be true but somehow it misses something.
** To me, calling something and getting an exception is NOT a bug,
** It is using an interface in a (hopefully) documented way.
My concerns about them include:
as used on the ground, the exception handling often occurs multiple levels up, and then it's not handled after all.
Used in a single class or method, the good case and the bad case or separated in the code and even in a single class we often don't really know where the exception came from. Either way the code flow is inherently obfuscated.
Exceptions are often used to pass the buck instead of dealing properly with an unusual condition at the level where it could best be dealt with.
The alternative to exceptions is not "return a flag which you must then check all the time and that is a stupid pain in the ass".
The often better alternative is "return a result which can be interrogated if you wish to try something different or which can be returned blindly to the next guy up the chain, who has the same option". If no one looks at the object and deals with it, it embodies sensible, generally mostly null behavior. This generates code that works better, is easier to read, and discourages laziness.
Once you get into a language style that runs on exception thinking, you kind of have no choice. If you build the software better from the beginning, there is a choice and IMO it is much nicer.
Ron's point is valid. Exceptions are hard to follow.
But, on the other hand, exceptions work when callers buggily do not check for errors returned.
I want the best of both worlds.
This suggests using extended or wrapper types, like the Valid
template I have used for years in simulators. Let's be a bit more explicit, and call it Valid_or_Throw_Exception, although it is possible that there is no need for a difference - in fact, my Valids often throw exceptions if accessed when invalid. The only big reason for a new type Valid_or_Throw_Exception is that you might want to carry an error message around, in my usual stacked error message approach - whereas my ordinary Valids are stripped down to be as efficient as possible.
Something like, in C++
template Valid_or_Throw_Exception
: public Wrapper_Type
{
bool valid = false;
string errmsg;
public:
Valid_Valid_or_Throw_Exception(const T& init) : Wrapper_Type(init), valid(true) {}
Valid_Valid_or_Throw_Exception() : valid(false), errmsg("uninitialized") {}
Valid_or_Throw_Exception Error(string msg) {
Valid_or_Throw_Exception ret;
ret.valid = false;
ret.errmsg = msg;
return ret;
}
private:
virtual void wrapper_pre_check() {
if( ! this->valid ) {
throw "accessing invalid data item returned as an error by ..." + this->errmsg;
}
}
}
Where Wrapper_Type
is a template that wraps the base type T, and arranges for all of the methods, etc., of T to be callable on the object that is wrapping. In this case, first calling wrapper_pre_check().
(I may have played a bit fast and loose here. My Valid
s so far have always been performance critical, so I would never have called a virtual function inside them. (Yes, I have measured the performance - that's what I do.) But outside of performance critical code, this seems reasonable.)
This allows values to be returned. If the values are good, then no worries. If the values are bad, exceptional, indicating an error, then the caller can still check. Or arrange to pass up the call stack nicely without checking, copy around, etc.
But if somebody tries to use the value without handling a possible error that actually occurred, bang!
But at least then you get the stacked error messages, which are nicer than not.
--
This makes me feel better about my nested exception string messages. They are useful when you aren't really handling error exceptions, but are just trying to provide the most meaningful error message possible. More context that "seg fault", but less context than a raw stack dump. (I have always imagined a clickable interface, so that you could see the outermost, top level, error message, and then click deeper and deeper if necessary.)
My preference is to throw strings. And only ever to throw strings, or possibly lists of strings in lieu of concatenation.
But this gets in the way of throwing proper exception objects for proper exception based error handling.
This hybrid approach gives both: well-behaved error handling via the objects returned, and nested exception string contexts when the well-behaved stuff fails.
===
Cool: attempting to reply to the [XP] mailing list via Yahoo's web posting facility gave the error:
PythonError: exception.NameErrot at d8c9c0
Which is an example both good and bad of exceptions.
It gets better -> cutting and pasting the error message into Blogger resulted in lossage because of angle brackets. That's the sort of "null behavior" that is annoying. I tweaked the error message.
First off, this is a fairly useless error message to provide to the user of a web page. I can only gues that NameError might be associated with my login name. Or maybe not.
Also, hex addresses like d8c9c0 are the sort of thing that make black hats drool. This *might* be a machine address. It gives me hints as to what addresses I might put into a code packet, if I could find a buffer overflow. This isn't a buffer overflow, but now I can go and look up Python exploits.
So far, Ron wins. It would be much nicer if the error message was something like
Bad login name: XXXXX
Turned out to be not having a profile. See how useless the Python exception is to the end user?
But... at least the error was checked and thrown. At least this was Python. In C++, it might have been unchecked, and there might have been a buffer overflow that a bad guy could use to break in.
It would be nice to have an appropriate error message. Ron's point about having the good and bad paths together wins: the programmer is much less likely to forget to handle the error if he can see "
return[ed] a result which can be interrogated".
But sometimes these errors occur deep, deep in library functions. Possibly libraries that presently assert. Sometimes changing the library is not an option, no matter how often one chants the XP "courage" mantra.
It's easy to #define assert to throw an exception. (Harder from a signal handler.) This gets you to Andrei's place.
And the next step is to my nested or stacked error messages:
PythonError:
exception.NameErrot at d8c9c0
User does not have Yahoo Profile: you must create one beforre possting.
Error trying to Post Message.
Not as good as what somebody following Ron's prescription could do. But better than nothing.
--
By the way, one of the big reasons why I was attracted to exceptions for error handling in C++ is that I found it made writing my own CppUnit equivalent (actually, not equivalent, minimalist) easy, especially when testing existing code that exit'ed or asserted on an error: rather than having to mess with multiple processes in ways that are often not portable, I culd usually #define assert to throw and/or use one of the exit hooks.
I.e. I used exceptions to make the test jig easier in the presence of ill behaved legacy code. And faster, fast enough to be used regularly - as opposed to forking processes around often very small test cases, which back in 1996, and even still today, produce horrible slowdowns, discouraging the team from using tests.