The problem:
Since long time, I am worried about the exceptions
mechanism, because I feel it does not really resolve what it should.
CLAIM: There are long debates outside about this topic, and most of them struggle at comparing exceptions
vs returning an error code. This is definitively not the topic here.
Trying to define an error, I would agree with CppCoreGuidelines, from Bjarne Stroustrup & Herb Sutter
An error means that the function cannot achieve its advertised purpose
CLAIM: The exception
mechanism is a language semantic for handling errors.
CLAIM: To me, there is "no excuse" to a function for not achieving a task: Either we wrongly defined pre/post conditions so the function cannot ensure results, or some specific exceptional case is not considered important enough for spending time in developing a solution. Considering that, IMO, the difference between normal code and error code handling is (before implementation) a very subjective line.
CLAIM: Using exceptions to indicate when a pre or post condition is not keep is another purpose of the exception
mechanism, mainly for debugging purpose. I do not target this usage of exceptions
here.
In many books, tutorials and other sources, they tend to show error handling as a quite objective science, that is solved with exceptions
and you just need to catch
them for having a robust software, able to recover from any situation. But my several years as a developer make me to see the problem from a different approach:
- Programmers tends to simplify their task by throwing exceptions when the specific case seem too rare to be implemented carefully. Typical cases of this are: out of memory issues, disk full issues, corrupted file issues, etc. This might be sufficient, but is not always decided from an architectural level.
- Programmers tends not reading carefully documentation about exceptions in libraries, and are usually not aware of which and when a function throws. Furthermore, even when they know, they don't really manage them.
- Programmers tends not catching exceptions early enough, and when they do, it is mostly to log and throw further. (refer to first point).
This has two consequences:
- Errors happening frequently are detected early in development and debugged (which is good).
- Rare exceptions are not managed and make the system to crash (with a nice log message) at the user home. Some times the error is reported, or not even.
Considering that, IMO the main purpose of an error mechanism should be:
- Make visible in code where some specific case is not managed.
- Communicate the issue runtime to related code (at least the caller) when this situation happens.
- Provides recovery mechanisms
The main flaw of the exception
semantic as an error handling mechanism is IMO: it is easy to see where a throw
is in the source code, but absolutely not evident to know if a specific function could throw by looking at the declaration. This bring all the problem that I introduced above.
The language do not enforce and check the error code as strictly as it make for other aspects of the language (e.g. strong types of variables)
A try for solution
In the intention of improving this, I developed a very simple error handling system, which tries to put the error handling at the same level of importance than the normal code.
The idea is:
- Each (relevant) function receive a reference to a
success
very light object, and may set it to an error status in case. The object is very light until a error with text is saved. - A function is encouraged to skip its task if the object provided contain already an error.
- An error must never be override.
The full design obviously consider thoroughly each aspect (about 10 pages), also how to apply it to OOP.
Example of the Success
class:
class Success
{
public:
enum SuccessStatus
{
ok = 0, // All is fine
error = 1, // Any error has been reached
uninitialized = 2, // Initialization is required
finished = 3, // This object already performed its task and is not useful anymore
unimplemented = 4, // This feature is not implemented already
};
Success(){}
Success( const Success& v);
virtual ~Success() = default;
virtual Success& operator= (const Success& v);
// Comparators
virtual bool operator==( const Success& s)const { return (this->status==s.status && this->stateStr==s.stateStr);}
virtual bool operator!=( const Success& s)const { return (this->status!=s.status || this->stateStr==s.stateStr);}
// Retrieve if the status is not "ok"
virtual bool operator!() const { return status!=ok;}
// Retrieve if the status is "ok"
operator bool() const { return status==ok;}
// Set a new status
virtual Success& set( SuccessStatus status, std::string msg="");
virtual void reset();
virtual std::string toString() const{ return stateStr;}
virtual SuccessStatus getStatus() const { return status; }
virtual operator SuccessStatus() const { return status; }
private:
std::string stateStr;
SuccessStatus status = Success::ok;
};
Usage:
double mySqrt( Success& s, double v)
{
double result = 0.0;
if (!s) ; // do nothing
else if (v<0.0) s.set(Error, "Square root require non-negative input.");
else result = std::sqrt(v);
return result;
}
Success s;
mySqrt(s, 144.0);
otherStuff(s);
saveStuff(s);
if (s) /*All is good*/;
else cout << s << endl;
I used that in many of my (own) code and it force the programmer (me) to think further about possible exceptional cases and how to solve them (good). However, it has a learning curve and don't integrate well with code that do now use it.
The question
I would like to understand better the implications of using such a paradigm in a project:
- Is the premise to the problem correct? or Did I missed something relevant?
- Is the solution a good architectural idea? or the price is too high?
EDIT:
Comparison between methods:
//Exceptions:
// Incorrect
File f = open("text.txt"); // Could throw but nothing tell it! Will crash
save(f);
// Correct
File f;
try
{
f = open("text.txt");
save(f);
}
catch( ... )
{
// do something
}
//Error code (mixed):
// Incorrect
File f = open("text.txt"); //Nothing tell you it may fail! Will crash
save(f);
// Correct
File f = open("text.txt");
if (f) save(f);
//Error code (pure);
// Incorrect
File f;
open(f, "text.txt"); //Easy to forget the return value! will crash
save(f);
//Correct
File f;
Error er = open(f, "text.txt");
if (!er) save(f);
//Success mechanism:
Success s;
File f;
open(s, "text.txt");
save(s, f); //s cannot be avoided, will never crash.
if (s) ... //optional. If you created s, you probably don't forget it.