The general solution to error handling is: write code that doesn't generate errors. Then there's no problem :P
Most of the time you can actually do this. 'Error handling' should be a very rare thing.
Oh, and since you have a strong opinion on error-handling but I seem to lack why, could you elaborate this to me?
Error handling is a name given to many different things. It could mean user input sanitization, it could mean code validation / quality assurance, it could mean any regular expected branching condition. Bundling all these different things up into a common name is bad. Trying to build a common solution to all of them is worse. Trying to codify that solution into a framework is evil.
Don't get caught up in false generalizations. Instead, approach the actual specific problems that you have.
Create something like an error-base-class and use namespacing to provide specific empty error-classes that derive from that? Or are enums a better solution?
This is evil framework over-engineering. It's not a solution to a specific problem.
So you are using something like a file-writing state machine plus lambda?
Mu. For what problem? That was just an example of how a "writing to file" problem could be restructured to avoid the need any error return codes whatsoever.
Every specific problem has many specific solutions do it.
For something like reading variables from a config file, where maybe entries are missing or corrupt, but each needs to be read no matter what, a decent solution would be to pass a default value into the read function. e.g. instead of:
bool ok = read("health", &m_health); if(!ok) m_health = 100;
ok = read("speed", &m_speed); if(!ok) m_speed = 42;
I'd go for:
m_health = read("health", 100);
m_speed = read("speed", 42);
As this is a nicer API for the caller, in this specific situation. In a different situation, the nicest solution will be different. Also, note that this isn't really error handling -- the default values for missing config entries is a feature of the system, it's an expected code branch, not some exceptional condition.
All of the rest of your code is full of features and expected branches, but you don't try to force a common solution onto every other if statement in your program. If you forget about "error handling" as a concept and just force these things to exist as features and expected conditions, then it's exactly the same as writing any other bit of code.
For example, perhaps a different problem is writing save-games to disk. Perhaps the best solution there happens to be a transactional database model. Transactions wrap up lots of smaller modifications and then tell you whether the whole things was successful or not. Recovery from error is hidden inside the transaction system implementation.
So instead of:
void WriteSaveData()
{
Error err = Write("health", m_health);
if( err != OK )
return err;
err = Write("speed", m_speed);
if( err != OK )
return err;
}
void Save()
{
Error err = WriteSaveData();
if( err == WRITE_FAIL )//shit we wrote a corrupt file, recover the old one
RecoverPreviousSaveGame();
if( err != OK )
ShowAppologyDialog();
}
You'd write something more like:
void WriteSaveData()
{
Write("health", m_health);
Write("speed", m_speed);
}
void Save()
{
Transaction t;
BeginTransaction(&t);
WriteSaveData();
EndTransaction(&t);
if( !t.Complete() )
ShowAppologyDialog();
}
There isn't a one-size-fits-all solution... or if there is, it's only going to fence you into writing sub-optimal code.
Additionally, you mention that I try to return multiple different types and I should not do this, but then you say std::variant is decent solution, however isn't the std::variant simply doing exactly that - returning multiple possible types but wrapped?
Yes. I said that in general you should avoid writing code that could potentially do two different things where the user doesn't know which, causing the user's call site to contain a bunch of complexity in order to deal with this fact... These complex call-sites are a cause of bugs -- that's why I think they should be avoided.
But, when you are forced to write such code, do it in a way that makes this fact painfully obvious at the call-site so that the chance of coding errors is diminished. If you are forced to write such code, then yeah, a variant will force this fact to be visible at the call-site, as they can't just write something like below -- Say we've got a function, DoThing, which can either produce a Result or an error.
//in the header:
bool DoThing(Result* output);
//in the cpp file:
Result r;
DoThing(&r);//the fact that perhaps this can fail is not obvious here! That's bad! Wrong code should look wrong!
r.OtherStuff();//bug: no errors are handled.
Using a variant means the caller is forced to write something like:
auto v = DoThing();
Result* r = std::get_if<Result>(&v);
if( r )
r->OtherStuff();
//The missing else case is an obvious code smell! You can now see that this code is missing an "error handling" branch
Using exceptions instead ensures that things can't be left in an invalid state and it automatically destroys every object in the same scope as the exception occurs (very good for resource managment).
That's only true if all of your code provides the Strong exception safety guarantee, which most C++ code really does not. Most decent C++ code will provide the Basic exception safety guarantee, which ensures that all your objects get destroyed and no resources are leaked, but does not ensure that all your class invariants are maintained; classes may well be left in invalid states. After catching the exception, you must take care to also safely destroy any classes that may have invalid invariants, which means you somehow have to know which classes they are, too...
Writing code that provides the strong guarantee is extremely hard in many common situations, and causes you to keep exceptions in the front of your mind when writing every single line of code. The mental tax of this is much higher than other alternatives.