Jump to content
  • Advertisement
Angelic Ice

Questions about Error-Handling

This topic is 551 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Hello forum!

I wanted to design error-handling in my application.

I heard that one shall use exceptions, when the behaviour of a code-segment can fail due outer dependencies, e.g. the OS/hardware operations being an outer dependency. One example would be writing or reading from a disk. The application could lack write-permissions or the disk could be out of memory.

But these OS-interactions are usually handled already. A file opened via std::ofstream can be check via is_open(). And acquiring known file-paths on Windows can be done via SHGetKnownFolderPath, returning a Result-object for the application to check. Therefore I assume using exceptions would be not required in this case.

Let's imagine we want a write_to_file-function, it could look like this: void write_to_file(const std::string file_path, const std::string content) const

It implements some required writing-methods that might return false or an error-object at some point. Therefore, it might be useful to know whether our write_to_file-function actually succeeded, I would change it to such (change is marked bold):

const bool write_to_file(const std::string file_path, const std::string content) const

Now the rest of the application can initiate a recovery upon failure. Sadly, generalising a failure to a boolean can be quite ambiguous, especially when there are multiple possible recovery-attempts of which only one might work per error-type. Generally, we would require no return-value for our function to even use in order to return a boolean.

If we would want to read from a file, the whole thing might become a bit different. Let's assume this is our read-function returning a file's content:

std::string read_from_file(const std::file_path) const

One option would be the following:

bool read_from_file(const std::file_path, std::string file_content) const

Instead of returning a file's content, we provide an empty string that gets loaded with a file's content. 

Solution to the generalisation via bool: Replacing the boolean-type with an error-object that could contain certain information about the actual error might solve the ambiguous meaning of a false.

Solution to the "return-type is already in use by the function: In the Rust language, we could return std::result<T, E>, which is similar to what std::expected<T, E> in C++17 should have become. If I understood std::expected the right way, I could write:

std::expected<std::string, Error_Object> read_from_file(const std::file_path) const

But as it seems, std::expected won't arrive. Alternative would be std::variant. Is there any way to actually use something as std::expected?

Is the general idea of this normal error-handling? Is there any common practice?

And after all of this, what are modern usages of exceptions within C++? Are exceptions a huge factor for the final size of the application and therefore a bit too much for small-size-applications? Speed is not so important for me.

Share this post


Link to post
Share on other sites
Advertisement
Then what? Are you going to handle the error immediately and recover from it then and there, or are you going to unwind the stack until you come to some code that does know how to handle the situation?

Good question, well pretty much depends on the actual function, I assume (since all I mentioned was just an example).

Sometimes the object that calls the potentially failing function could handle it and other times it won't be able and requires other objects to resolve the error.

If writing to a savegame-file fails, that can be quite a devastating error. The class would probably check whether memory is full or write-permission is missing, then inform the user via some popup and simply continue the application, try it again later.

But if reading from a savegame-file fails, it probably depends. If the file does not exist any more, I would simply start a new game for the user. If read-permissions are gone (for whatever weird reason) during a level-transition, I would try to enforce a save and return to the main-menu. Otherwise, if it happens upon starting a level the first time, I would simply notify the user and do nothing.

Seems like I have to get my mind clear about what handles how.

But assuming I would not require an exception, is going with something like a std::variant as return-value okay? Having the value of what I actually want to return and an error-class as the other possible type.

That varies. An exception that does not get thrown has almost no runtime impact on modern C++ and the additional size introduced is negligible.

What people told me, is that I should try to avoid them whenever I can check potential exception-triggers beforehand. Example, division by 0. Rather check and reject via basic if-flow-control instead of exceptions.

Edited by Angelic Ice

Share this post


Link to post
Share on other sites
The general solution is: don't. Whenever possible, don't write functions that can possibly do/return two different things. Document preconditions and postconditions and validate them with assertions. Move "error handling" from a runtime problem to a architecture time problem. An assertion failure at runtime tells you that your code is incorrect, which is mot recoverable without a programmer in the loop. Strive for correct code.

So you are using something like a file-writing state machine plus lambda?

About the std::variant, what would you exactly use? Create something like an error-base-class and use namespacing to provide specific empty error-classes that derive from that? That would allow to check whether the class is the assumed result or an error-class. Or are enums a better solution?

E.g. a file_to_string-function would return a std::variant<std::string, err::Error>, assuming the name space is err.

That is at least what I learned from Rust - of course, this is C++, Rust idioms and patterns are by no means always good or possible to apply to C++.

Does the lambda-"sidestep" of the std::variant-return-pattern have a name?

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?

Oh, and since you have a strong opinion on error-handling but I seem to lack why, could you elaborate this to me?

Edited by Angelic Ice

Share this post


Link to post
Share on other sites

This is well covered in the error handling chapter of Bjarne Stroustrop's book The C++ Programming Language. It's a very good book and I recommend that you read it.

To give a short and general answer: Use exceptions whenever an error occurs. That is, whenever something that is not supposed to happen happens. Or what Stroustrup says: When something "exceptional" occurs (something out of the ordinary, could be things other than errors).

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). It also leaves you without the hassle of having to check return values all the time (this is what I don't like about programming in SDL).
 

What people told me, is that I should try to avoid them whenever I can check potential exception-triggers beforehand. Example, division by 0. Rather check and reject via basic if-flow-control instead of exceptions.


So what exactly does 'reject' mean here? Just continue as if nothing wrong happened? It definitely makes sense to throw an exception in this context, unless you have a valid answer to a zero division in the same scope as it occurs in.

If an error can only occur because the programmer obviously did something wrong, then yeah, using asserts instead makes sense.

A good way to know how you should do error handling is to think to yourself for every error that occurs: What is supposed to happen when this error occurs? Does this function know what should happen? Should the caller of the function decide what will happen? Should the program just crash? Exceptions can't and shouldn't be used all of the time, but if the place the error occurs in doesn't know what should happen, throwing an exception is usually a good way to handle it. Also, don't use try/catch everywhere, not every exception is meant to be caught by every part of the program.

Share this post


Link to post
Share on other sites

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.

Edited by Hodgman

Share this post


Link to post
Share on other sites

As ever so often, there is no clear-cut always-correct rule. Return codes can be the right thing, and exceptions can be. In a perfect world, things never fail, but in reality they can, and will.

Exceptions have a bad reputation because they're (presumably) expensive and because a lot of people use them badly, or because people feel like they are unintuitive with all the stack unwinding and stuff.

Exceptions are not altogether "free", in particular they add around 50kB of boilerplate code to your executable, but they are nowhere near as abysmal performance-wise as often propagated, in particular not as long as none are thrown (which by definition, happens exceptionally, not all the time).

What's good about an exception is that it's not just about printing out "fail" and bailing out, but it allows you to roll back a whole set of operations to a clean state from which you can possibly do something useful. And, you are actually forced to do something in reaction to bad things that happen (you cannot just ignore them) because failure will not be silent (that's a good thing).

In your example of a write failing, handling the error is arguably much easier with a simple boolean, at first sight.

Except, you might have not one write but a dozen writes (if the first one succeeds, the second or third could still fail), and maybe you don't want half-written junk files, etc.etc.

Or let's think about something more hazardous, like a Vulkan resource that you need to close after a failure. You know, who cares... when a process terminates, the OS will reclaim all resources anyway -- closing a Vulkan device is for pussies. Tell you what, on my machine with a nVidia card, you can do this 10-12 times, no problem, and half an hour later when you don't expect it, the display driver will crash! So yeah, you definitively want to make sure you have correctness in presence of failure guaranteed.

The elegant thing about using an exception (and proper RAII) is that you are able to do a dozen things as if no possibility of an error even existed. But if the tenth thing fails after the previous nine succeeded (and an exception is thrown), then proper cleanup happes as if by magic (i.e. junk files are deleted and handles closed, or a device is destroyed), and you can try again from a clean state.

Or, if nothing helps, you can still bail out. But that should be the last choice, not the first. Proper cleanup and reset to a sane state with return codes is possible too, of course. But it's not nearly as pretty nor as easy to get right.

On the other hand, return values are nice because they are super efficient. And, well, because you can ignore them. Which, on second sight, is not as nice as it sounds (in fact, that's not nice at all, it's desastrous). Unluckily. there's no way you can force the user of a function to even check whether an error occurred (as of C++17 there is now the nodiscard attribute... but this only produces a warning, so it is only good for accidential forgetness, if you are stubborn enough you still can fail to check for errors).

 

 

C++ assertions are so bad that you should just pretend they don't exist as a language feature.

That is correct as far as error handling goes, but using assertions for runtime error handling would be somewhat ill-advised, too. It's not what their purpose is.

For the purpose that assertions were designed for -- ensure program correctness, guarantee that preconditions that may not occur in a correct program indeed do not occur -- they are in my opinion very valuable.

Edited by samoth

Share this post


Link to post
Share on other sites

That is correct as far as error handling goes, but using assertions for runtime error handling would be somewhat ill-advised, too. It's not what their purpose is. For the purpose that assertions were designed for -- ensure program correctness, guarantee that preconditions that may not occur in a correct program indeed do not occur -- they are in my opinion very valuable.
Ugh, that's a typo -- I meant to say "exceptions", not "assertions".

Yes, assertions are to ensure program correctness. Every single unexpected/invalid condition should be enforced/validated with an assertion.

Exceptions are for valid but exceptionally unexpected, yet somehow still a little bit expected conditions :lol:
...and in C++ they're so bad that you should just pretend they don't exist as a language feature.

Share this post


Link to post
Share on other sites

The basic pattern can be summed up as functions that return a variant<Result, Error>. That's a decent solution, and lets you assert at runtime that the code at least checked for the existence of an error instead of ignoring them.
 

 

And this is where the functional world has it right although it brings in the word which terrifies.... monads lol

With something like an Either<TR, TL> you get the error handling and the added benefit that your code is clean as you do not have if/case etc in your flow to determine if you are in error state or not.

And I agree with you totally, it is possible to write good code that does not fault when you push the side effects to the edges of your code. 

Share this post


Link to post
Share on other sites

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!