Angelic Ice

Questions about Error-Handling

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
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

I've changed from returning a bool if something goes wrong to crashing if it's not something that should have a fail state.  For instance loading a players custom map, if it doesn't exist and it's manually typed in for some reason; it's not "crash the whole program" worthy to me.  But if we're missing a common texture from the game, we crash with a very specific message and if you look one level up in the call stack you see exactly where it broke, and can quickly find the issue.  This way the programmer, me, cannot be lazy and fix it later.  It's resolved ASAP and gets the designer and artists back to work as fast as humanly possible with a now functional game.

I also go with simple functions that do one thing, and one thing only.  I hear of 2000+ LOC functions and want to scream at them to break it up.  I might have limited vision but I've never seen an instance where a few dozen functions could be created to wrap that code up with the same functionality.  It makes it far more readable and I find, easier to debug as you can test each function separately if need be, in most cases.

I also, now, heavily assert my code where logical.  I found a custom assert online from someone and modified theirs but I can make it active even in release builds (with a re-compile) for testing in release.  I shun C++ exceptions and have them disabled in all my projects.  I can personally see a use case for C# ones in the code I've reviewed, but the C++ ones just seem to broken to me.  My solution I find is easier to code too.  Hodgman influenced me into this sort of coding a couple years back on a similar topic if memory serves me correctly (it's a rarely event mind you).

Share this post


Link to post
Share on other sites
My share;
I think you should first think about the functional path/ what you want to happen in the possible cases that can occur, then based on that you choose how to handle those situations (case by case). Which can be returning a bool, the created object etc.

Personally I don't use exceptions at all. Just some incidental asserts, for debugging

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 Most of the time you can actually do this. 'Error handling' should be a very rare thing.

On game consoles this is a strict requirements. When you're developing for console and PC, often this results in much better PC code.

For example, before writing to disk you should verify you have space, then allocate the space and verify it, then write the content to the space.  

Another, for system calls that return error codes, verify the result of every call.  If a drawing call can return an error, verify it and take actions every time. Even crazy simple stuff sprintf() calls, verify the result. 

Write code where they only way there can be a failure is if something is critically wrong with the system. Be a defensive programmer.

Share this post


Link to post
Share on other sites

Had a whole long post completely lost...  Really short form:
We had this discussion before see here https://www.gamedev.net/topic/674790-c-exceptions/.
The whole notion of 'write bug free code' I find disingenuous.  Calling assertions 'not bugs' is still error handling.  Too much arguing over semantics IMO.
I prefer exceptions over error codes since it gives me choice at the call site.  If I catch an exception, its a recoverable exception, it I don't its an 'assertion'.  Many times you don't know when writing the code that causes the error whether that error should cause an assert or is recoverable.  Exceptions give you a clean and efficient way to express that choice.
 

Share this post


Link to post
Share on other sites

Had a whole long post completely lost...  Really short form:
We had this discussion before see here https://www.gamedev.net/topic/674790-c-exceptions/.
The whole notion of 'write bug free code' I find disingenuous.  Calling assertions 'not bugs' is still error handling.  Too much arguing over semantics IMO.
I prefer exceptions over error codes since it gives me choice at the call site.  If I catch an exception, its a recoverable exception, it I don't its an 'assertion'.  Many times you don't know when writing the code that causes the error whether that error should cause an assert or is recoverable.  Exceptions give you a clean and efficient way to express that choice.
 

There is next to zero chance to write perfectly formed exception safe code.  Weird library code can fire off weird exceptions.  I'll likely never find it again but there was an awesome article by someone in the games industry who said turn them off.  Code like frob mentions.  Check every return code of everything.  Assertions ARE bugs.  I've not actually seen anyone say otherwise that was not a complete noob honestly.  But I might have missed something.

Share this post


Link to post
Share on other sites

Several people have made that kind of statement. These days the language has evolved enough that the general guideline is not to turn them off completely, but instead to not use them except in truly unavoidable situations.  And it is nearly always avoidable.

John Spicer, the guy who wrote some big chunks of the C++ language standard, called exceptions a "shadow type system".  That is, the language directly provides strictly enforced types, those are checked in parameters, checked in return results, checked in assignment and comparisons, and so on, all enforced at compile time so you know if you messed up.  But exceptions are a "shadow" system, they are not enforced in C++, and since they must be enforced by humans are frequently done wrong.

When it comes to trying to limit them, the authors of Boost wrote: The biggest problem with exception-specifications is that programmers use them as though they have the effect the programmer would like, instead of the effect they actually have.

Herb Sutter, the guy who has spent over two decades over the C++ language standard committee, wrote quite a lot on them and the many subtle issues they cause.  A few of the articles: 1 2 3 4 5 6 and there are many more.

The details of the exception system in C++ are nuanced and subtle. A tiny change can have an enormous penalty for performance. Attempting to ensure your program does the 'right thing' with exceptions is quite likely to cause your program to unexpectedly terminate rather than behave as expected. Do something wrong and the exception system calls std::unexpected() or std::bad_exception(), both of which default to immediately terminating your program, other exception violations don't even go through those functions, they jump straight to termination.  There is no ability to handle it, there is no way to install a crash handler, or to call atexit() functions.  The program is instantly and fatally dead with no chance of recovery, logging, or reporting.

 

These days library writers are somewhat better at using exceptions, and experienced developers generally know enough to avoid the biggest landmines, but there are still many companies who follow the old advice of disabling all exceptions, including those generated and used within the C++ standard library.

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 Most of the time you can actually do this. 'Error handling' should be a very rare thing.

On game consoles this is a strict requirements. When you're developing for console and PC, often this results in much better PC code.

For example, before writing to disk you should verify you have space, then allocate the space and verify it, then write the content to the space.

 

That's all nice in a perfect, imaginary world, but in the real world not generating errors is an illusion. You cannot write code that cannot generate errors because errors do happen. Errors, or failures, are a normal condition ("normal" not in the sense that it occurs all the time, or even often... but in the sense that it can occur at any time, it is legitimate for an operation to fail).

Your example of checking for space prior to writing is a good example of how not to do it, even though many people do that and even though it works fine most of the time. However, "works fine most of the time" is not the same as "robust".

Checking for space, you get a snapshot of the current state. That's better than nothing, but it only loosely correlates with the ability to allocate or even write data the next instant, and that's all about it. Someone else can use up the volume's remaining space (physically or logically) the next instant, and your allocation will fail.

But even if you have already reserved space for your file, and even if you are only overwriting physically-existing clusters, you cannot be sure that the write will succeed. Not only do some filesystems as well as devices do reserve/allocate/commit in a weird manner (think some CoW filesystems, think SSDs), but the device might find a bad block and fail to relocate it. This is admittedly a rare thing, but in principle a "normal" condition that can occur at any time.

You can lose your DirectX or Vulkan device or swapchain at any time (well, not any time, but pretty much "any" time). Just assuming "will work" doesn't do. As you wrote yourself: "Check every return code" (with very, very few exceptions which only occur if things are so fucked up beyond hope that if they happen the user has an entirely different problem anyway, such as when the system tells you "out of physical memory" or "cannot create thread"). BeOS used to have a function is_computer_on_fire() to check whether the computer was burning. Few developers checked for that condition (which I guess was allowable).

Your TCP connection with "guaranteed delivery" can break at any time, even after the network layer has accepted your send (so you're guaranteed the data will be sent, right?). That's a bad example on my part since there's no way of telling before having closed the socket (and then, what do you do about it!), but my point is... failure does happen.

If you cannot deal with failures in a 100% robust way (either by "don't care" if you're lucky, or by a mitigation strategy), you are doomed. The above approach BeginTransaction(); Write(); EndTransaction(); is basically exactly what you would also do with exceptions, only you would likely use a scope_guard (or a construct around unique_ptr) as the building block.

Note however that "cannot create errors" is wrong. It certainly does create errors, but it deals with them in a robust, no-chicken-out manner.

Edited by samoth

Share this post


Link to post
Share on other sites

The details of the exception system in C++ are nuanced and subtle. A tiny change can have an enormous penalty for performance. Attempting to ensure your program does the 'right thing' with exceptions is quite likely to cause your program to unexpectedly terminate rather than behave as expected. Do something wrong and the exception system calls std::unexpected() or std::bad_exception(), both of which default to immediately terminating your program, other exception violations don't even go through those functions, they jump straight to termination.  There is no ability to handle it, there is no way to install a crash handler, or to call atexit() functions.  The program is instantly and fatally dead with no chance of recovery, logging, or reporting.

If my understanding is correct, any exception related errors call std::terminate() and you can set a custom terminate handler.  If you find yourself in a situation where this happens often you can install a custom terminate handler to get error/debugging info.

Edited by Ryan_001

Share this post


Link to post
Share on other sites

Crap

You're completely missing the point, or not seeing the forest for the trees.

For the trying to allocate some disk space examples, let's say the possibilities are that the OS either doesn't let us create the file, it creates the file with less bytes than we requested, or it creates the file with the correct number of bytes.
A typical "error handling" approach, using exceptions would look something like:

struct out_of_space_exception : std::exception { ... };
struct cannot_create_file_exception : std::exception { ... };
void allocate_file( const char* path, int size );// throws out_of_space_exception, cannot_create_file_exception

If you use return codes instead of exceptions, you can rearrange that to something like:

#define ERROR_SUCCESS 0
#define ERROR_OUT_OF_SPACE 0x87654321
#define ERROR_CANNOT_CREATE_FILE 0x87654320
typedef int ERROR;
ERROR allocate_file( const char* path, int size );//returns ERROR_SUCCESS, ERROR_OUT_OF_SPACE, ERROR_CANNOT_CREATE_FILE

But the "there are no errors" philosophy says to try to minimise the different types of results that can occur and streamline the caller towards easily dealing with them, so you'd end up with something like:

int try_allocate_file( const char* path, int size );//returns the number of bytes allocated, or -1 if the file couldn't be created

In contrast to the "error handling" philosophies, the "error" conditions aren't defined as exceptional or error conditions. Instead, the "error" conditions are normal (expected) values within the defined output domain. IMHO, making them expected values increases the likelihood of them being handled correctly. Furthermore, the fact that this is a request to the OS is made perfectly clear to readers at the call-site by clear naming, in this case the "try_*" prefix. If the call-site doesn't handle the return code, this prefix is a code-smell to readers, making the mistake stand out ("if this is a request, how is the result checked here?").

Of course the same "errors" can occur. The difference is in the label that you put on them.
One camp says that "allocate_bytes" can generate error conditions, including total failure or incorrect number of bytes.
The other camp says that "try_allocate_bytes" can allocate an unpredictable amount of bytes, including less than none.

Another way to word the philosophy might be "don't treat errors as being exceptional", or "make failures expected"

And yes, if you adopt this philosophy you can rearrange the vast majority of your code so that there are no "errors" at all, by moving the parts that can fail (e.g. OS requests) off into their own corner, and then making the rest of your code error-less. Exceptions have the opposite effect, where you have to treat almost every single function call in the entire program (including operators that can be overloaded) as if it could potentially trigger an error condition that must be handled. To pretend that there is no difference there, and that the real-world effects on your architecture are imaginary and not concrete is to indulge in significant ignorance or farce.

Share this post


Link to post
Share on other sites

 

Crap

You're completely missing the point, or not seeing the forest for the trees.

For the trying to allocate some disk space examples, let's say the possibilities are that the OS either doesn't let us create the file, it creates the file with less bytes than we requested, or it creates the file with the correct number of bytes.
A typical "error handling" approach, using exceptions would look something like:
struct out_of_space_exception : std::exception { ... };
struct cannot_create_file_exception : std::exception { ... };
void allocate_file( const char* path, int size );// throws out_of_space_exception, cannot_create_file_exception
If you use return codes instead of exceptions, you can rearrange that to something like:
#define ERROR_SUCCESS 0
#define ERROR_OUT_OF_SPACE 0x87654321
#define ERROR_CANNOT_CREATE_FILE 0x87654320
typedef int ERROR;
ERROR allocate_file( const char* path, int size );//returns ERROR_SUCCESS, ERROR_OUT_OF_SPACE, ERROR_CANNOT_CREATE_FILE
But the "there are no errors" philosophy says to try to minimise the different types of results that can occur and streamline the caller towards easily dealing with them, so you'd end up with something like:
int try_allocate_file( const char* path, int size );//returns the number of bytes allocated, or -1 if the file couldn't be created
In contrast to the "error handling" philosophies, the "error" conditions aren't defined as exceptional or error conditions. Instead, the "error" conditions are normal (expected) values within the defined output domain. IMHO, making them expected values increases the likelihood of them being handled correctly. Furthermore, the fact that this is a request to the OS is made perfectly clear to readers at the call-site by clear naming, in this case the "try_*" prefix. If the call-site doesn't handle the return code, this prefix is a code-smell to readers, making the mistake stand out ("if this is a request, how is the result checked here?").

Of course the same "errors" can occur. The difference is in the label that you put on them.
One camp says that "allocate_bytes" can generate error conditions, including total failure or incorrect number of bytes.
The other camp says that "try_allocate_bytes" can allocate an unpredictable amount of bytes, including less than none.

Another way to word the philosophy might be "don't treat errors as being exceptional", or "make failures expected"

And yes, if you adopt this philosophy you can rearrange the vast majority of your code so that there are no "errors" at all, by moving the parts that can fail (e.g. OS requests) off into their own corner, and then making the rest of your code error-less. Exceptions have the opposite effect, where you have to treat almost every single function call in the entire program (including operators that can be overloaded) as if it could potentially trigger an error condition that must be handled. To pretend that there is no difference there, and that the real-world effects on your architecture are imaginary and not concrete is to indulge in significant ignorance or farce.

 

 
No one's arguing that if we can write code without errors/exceptions/whatever that we shouldn't do so.  But just calling errors something else, doesn't change the method of handling them.  For example, how are these any different?

if (try_allocate_file(file_name, file_size) != ERROR_SUCCESS) {}
if (try_allocate_file(file_name, file_size) != file_size) {}

Its the same thing, its still an error return code just called something different.

In the original discussion we had on this you posted a link to this article http://joeduffyblog.com/2016/02/07/the-error-model/.  I read it and found it interesting.  It even gave this quote:

Anders Hejlsberg : No, because in a lot of cases, people don’t care. They’re not going to handle any of these exceptions. There’s a bottom level exception handler around their message loop. That handler is just going to bring up a dialog that says what went wrong and continue. The programmers protect their code by writing try finally’s everywhere, so they’ll back out correctly if an exception occurs, but they’re not actually interested in handling the exceptions.

 He calls it a 'head scratcher'.  Now if writing an OS or a driver, ya that's a bad idea, but for a user application, 99% of the time that makes perfect sense.  After going through all the rigmarole he decides on two things.  First is to call bugs as 'not errors':

Given that bugs are inherently not recoverable, we made no attempt to try. All bugs detected at runtime caused something called abandonment, which was Midori’s term for something otherwise known as â€œfail-fast”.

Given that he calls the original quote a 'head-scratcher' and then basically does the exact same thing...  I'm sorry, this is just a semantic game he's playing.  Its not really an 'error' its an 'unrecoverable bug'...  And if ignoring an exception is bad, then how is automatic program termination any less bad?  At least in the case of the exception you can attempt to unwind the stack and maybe save some of the data.  And as far as the assertion that he, you, and many others give that 'anything at any time can throw an exception', here's a list he came up with of things that are bugs that warrant 'fail fast':

  • An incorrect cast.
  • An attempt to dereference a null pointer.
  • An attempt to access an array outside of its bounds.
  • Divide-by-zero.
  • An unintended mathematical over/underflow.
  • Out-of-memory.
  • Stack overflow.
  • Explicit abandonment.
  • Contract failures.
  • Assertion failures.

And that's just the tip of the iceberg.  Talk about 'can fail anywhere'.  I don't see how you can criticize exceptions and then turn around and tout the same problem as a 'feature'.  This is nonsense.

Now I agree in that errors (and hence exceptions) can occur near anywhere.  But exceptions have an advantage over the 'std::terminate' method he and you advocate for: some can be caught if you want to catch them, because not all of these are unrecoverable under all circumstances.  So worst case scenario, exceptions are just as bad as 'unrecoverable bugs' best case they are better.  The reality lies in between but at least with exceptions you have the option.
I also find it ironic that in the end after calling many errors 'unrecoverable bugs', the rest of the time he advocates exceptions.  I also found this quote interesting:

The model I just described doesn’t have to be implemented with exceptions. It’s abstract enough to be reasonably implemented using either exceptions or return codes. This isn’t theoretical. We actually tried it. And this is what led us to choose exceptions instead of return codes for performance reasons.

Time and time again I hear people state that exceptions are slow.  This is not true.  Exceptions are faster than checked return codes on any modern compiler; and if you're not checking your returns codes, then exceptions are safer.  With RIAA they are almost always faster and safer.

So in short:
 - Errors occur, whether you call them errors, assertions, exceptions, 'unrecoverable bugs', or otherwise, its still a condition that can and does occur and must be handled.  Playing semantic games helps no one and leads to these ridiculously confusing debates.

- Errors can occur near everywhere.  Whether you choose to throw an exception, call std::terminate, or ignore them, doesn't make any difference to the frequency of these extraneous conditions occurring.  Using exceptions doesn't magically cause them to occur more often, calling them 'unrecoverable bugs' doesn't cause them to occur any less.

Share this post


Link to post
Share on other sites

 

Crap

You're completely missing the point, or not seeing the forest for the trees.

For the trying to allocate some disk space examples, let's say the possibilities are that the OS either doesn't let us create the file, it creates the file with less bytes than we requested, or it creates the file with the correct number of bytes.
A typical "error handling" approach, using exceptions would look something like
[...]
return -1 instead

 

I'm afraid you are the one missing the point. The point is that the OS will let us create a file, it will let us allocate a size, and it will let us write once, twice, and then it will fail. Or something.

The assumption "writes succeed because I checked for free space" is wrong, as is the assumption "writes will succeed because I allocated".

That's irrespective of whether you throw an exception or return -1. However, if you return -1 from your try_create_whatever function, you're pessimizing insofar as it is perfectly possible (and not just possible, but likely to happen) that you don't check for the result. Of course you don't, why would you. It's extra work, and nobody forces you to do it. Besides, writes always succeed anyway.

The exception model is a good fit here since it models what goes on: Most of the time, creating a file and writing to it just succeeds (assuming you don't try to write in a system folder or such). Most disks most of the time are not full, nor are there concurrent writes filling the disk most of the time, nor do disks fail most of the time. Thus... errors do not happen, except when they do happen, exceptionally.

With your -1 error code, chances are that you are just forgetting to check for the return code and your program will have undefined behavior, or it will "work fine" except it doesn't write anything to disk (or just a part of the data) and remains silent about it.

The part about remaining silent is what is really bad. It means things that possibly could be handled are never handled because you don't have to, and you don't know they happen anyway. All you know is the program somehow fucks up from time to time in some manner. Somehow every 500th of your documents, on the average, is corrupt, nobody can tell why.

Exceptions have the advantage that this cannot happen. You must write a handler, or the first time something goes wrong on anyone's computer (including yours while testing, but let's assume you were lazy and didn't bother), you get an angry user calling: "WTF does 'terminate called after unhandled cannot_allocate_filesize exception` mean???".

That doesn't help this particular user. However, it is immediately obvious what happened, and it is immediately obvious what you must do to fix your program. Now tell me returning -1 bears the same amount of information.

Share this post


Link to post
Share on other sites

The assumption "writes succeed because I checked for free space" is wrong, as is the assumption "writes will succeed because I allocated".

Nobody made that assumption. That's why I said you were missing the point. Give us some credit / good faith :P
If allocate returns some kind of locked file handle, then a single error condition is removed from the write -- that the disk is full. Plenty of other failure reasons exist (such as the OS closed your handle :P )

Arguing about error handling at the hardware boundary is not that useful, as the OS / low-level stack has already implemented all of the error checking and exposed a particular paradigm to us here.
The way that you deal with program state validation within your own code is much more interesting.
 

With your -1 error code, chances are that you are just forgetting to check for the return code and your program will have undefined behavior, or it will "work fine" except it doesn't write anything to disk (or just a part of the data) and remains silent about it.

Well, no, because if you go whole-hog with this philosophy then you would also put a precondition on the write function, which asserts that the file was allocated with the correct size. That means when you forget to handle the error condition, you get a big noisy assertion failure... which is the same as when you forget to implement a catch and you get an unhanded exception (but without the chance that some catch(...) will accidentally silence it). They're pretty equivalent, really.
The point of renaming it from blah to try_blah though was that it does make code that doesn't check the return value very obvious to the reader. With exceptions you can always dismiss it as a deliberate choice to pass the exception up the stack or not. A good exception specifier system (where each function declares what it can possibly throw) would make be really happy in that regard -- these choices would be documented as part of the code itself.

 

Exceptions have the advantage that [silent failure]cannot happen. ... Now tell me returning -1 bears the same amount of information.

If you want to assume slopping coding is a real world thing, then too-broad catch statements are definitely something that happens -- accidentally swallowing up exceptions and silently not handling them. In my experience, as the number of possible exceptions throughout the program grows, the incidence of this occurring rises. It also happens sometimes when two systems throw the same class, but one wants to throw a short distance up the stack while the other wants to throw a long distance (the short catch accidentally interrupts the long throw).

As above, the return -1 doesn't contain that information -- but the failure to handle the precondition for the upcoming function will trigger an assertion failure, which in any decent project will dump the current values of any related variables to the log, trigger a breakpoint in the debugger if one is connected, or perform a full memory dump to disk if one is not connected, which will allow a programmer to step in at a later time and start a debugging session with the program cursor paused on the offending line of code with the full stack and heap available to them. 

@Ryan_001 that's why we terminate early -- dump the full program state on the first line of code where invalid program behavior is detected, and programmers can find the bugs extremely quickly and get them fixed well before the release date of your game.

Errors occur, whether you call them errors, assertions, exceptions, 'unrecoverable bugs', or otherwise, its still a condition that can and does occur and must be handled.

No.
Assertions and 'unrecoverable bugs' occur when the program behaviour does not match the program specification.
Exceptions an 'runtime errors' are features of the program.
 

here's a list he came up with of things that are bugs that warrant 'fail fast':

Those are all indications of the program behaviour/specification disagreeing.
e.g. if the code is attempting to dereference a pointer, that means that the code has made the assumption that it is a valid pointer, which means that it's a precondition of this function that the pointer is valid.
Fair enough if you don't want such a precondition to exist, then you can declare a post-condition instead -- such as "if a NULL pointer is passed into this function, an invalid_argument exception will be thrown". That's fine, but you're changing the specification there. The point is that any time that the behaviour and the specification diverge, then you've got yourself buggy code, and should probably just halt before it does damage. 
 

Time and time again I hear people state that exceptions are slow.  This is not true.

Error handling where none is required is obviously slow.
If you can prove at compile time that your code will never generate an out of bounds condition, yet you perform bounds checking on every array lookup anyway just in case someone wants to tolerate that error, then yes, this is slower than not doing it.
The point of preconditions/postconditions/invariants and the whole quest for formal correctness is to move most errors to being a code-time affair instead of a runtime affair... which results in less lines of code to maintain, less complexity, and less instructions at runtime. 

And if ignoring an exception is bad, then how is automatic program termination any less bad?

An unhandled exception is just as bad as an assertion failure -- neither should occur in a well formed program. 
Exceptions get slugged here because they're being used for two purposes (recoverable errors and unrecoverable errors). If the program has detected that all of its assumptions are invalid and it's not behaving according to the specification, then it makes no sense to try to catch such an error (except as a "crash handler" that aids in shutdown / logging).

I don't see how you can criticize exceptions and then turn around and tout the same problem as a 'feature'.  This is nonsense.

Because exceptions actually have an impact on your code. Writing code that provides the Strong exception safety guarantee is extremely hard in many cases. This burden is applied to your entire code-base (or ignored, leading to the possibility of class invariants being violated, which is a terrible choice to make). Having the program terminate when it detects that an invariant has been violated (and thus, the code is wrong) does not put any burden on the rest of your code-base. The fact that you think this is nonsense implies that you're not aware of the different exception safety guarantee levels and their respective complications.

I'm sorry, this is just a semantic game he's playing

No, he's not just playing games with words. The meanings are extremely different.

Assertions are a tool to validate pre-conditions, post-conditions and class invariants. Every program has these. Programs with excessive use of exceptions will have less of them than other programs, but they still exist.
For an example, take a simple array class:

struct MyVector
{
  //class invariants: size >= 0 && is_mapped_as( READ_WRITE, data, data+size )
  T* data;
  int size;

//VERSION 1:
  //preconditions: i>=0 && i<size
  T& operator[](int i) { 
    assert(size >= 0 && is_mapped_as( READ_WRITE, data, data+size ));
    assert(i>=0 && i<size);
    return data[i];
  }

//VERSION 2:
  //postconditions: if i<0 || i>=size then an out_of_bounds exception is thrown
  T& operator[](int i) {
    assert(size >= 0 && is_mapped_as( READ_WRITE, data, data+size ));
    if(i>=0 && i<size)
      return data[i];
    throw out_of_bounds(i, size);
  }
};

Preconditions, postconditions and invariants aren't up for debate as a concept. They're the software engineering foundations that we're building on top of.

Version #1, above, makes error checking and handling the caller's problem by simply specifying that the arguments MUST be correct. If your code happens to fit well with this (e.g. you're just doing iteration, etc, not getting indices from the user), then this will work just fine.
Version #2, above, does internal error checking and allows the user to handle the out-of-bounds errors if they want (or ignore them and maybe let them bubble up to main).

Both are valid things to do, and both have pre-conditions, post-conditions and invariants. If you do have these things, you should document them with assertions. Some earlier languages actually forced you to document these assumptions in code, but most modern languages do not, so assertions take on that role now.

It's often argued in C++ that classes should be as simple as possible, and any functions that can be implemented in terms of the public interface should be removed from the class and implemented as a free function.
If you agree with that philosophy, then Version #2 can be implemented in terms of version #1:

T& try_get( MyVector& input, int i )
{
  if( i >=0 && i < input.size )
    return input[i];//preconditions provably satisfied, this will never cause an error
  else
    throw out_of_bounds(i, input.size);
}

The exceptions-everywhere crowd (*cough* Java) just like to move errors out of pre-conditions/invariants/post-conditions and move them into runtime checks.
The no-errors crowd like to rely on pre-conditions/invariants/post-conditions as much as possible to prove at build-time that errors cannot occur, and therefore don't require runtime checks.

Edited by Hodgman

Share this post


Link to post
Share on other sites

Errors occur, whether you call them errors, assertions, exceptions, 'unrecoverable bugs', or otherwise, its still a condition that can and does occur and must be handled.

No.
Assertions and 'unrecoverable bugs' occur when the program behaviour does not match the program specification.
Exceptions an 'runtime errors' are features of the program.

Assertions are errors.  You can't claim otherwise.

They are conditions that should not occur, but do occur.  Just because you called it an 'assertion' doesn't mean it doesn't occur, can't occur, or doesn't need to be dealt with at run time.  The fact that you classify exceptions as 'runtime errors' in contrast to assertions implies that you are stating that assertions are in fact not runtime constructs.  This is wrong.  Assertions, unrecoverable bugs, exceptions, and whatever else are all runtime errors.  Whether that error is due to hardware problems, breaking programming contracts/behavior that does not match program specification, whatever the source, they all fall under the blanket term programming error or 'bug'.

Assertions, like error return codes and exceptions, are a method for handling run time errors (now I'm not saying there aren't static assertions, which are great things I use everywhere, but they are a special case).

Those are all indications of the program behaviour/specification disagreeing.
e.g. if the code is attempting to dereference a pointer, that means that the code has made the assumption that it is a valid pointer, which means that it's a precondition of this function that the pointer is valid.
Fair enough if you don't want such a precondition to exist, then you can declare a post-condition instead -- such as "if a NULL pointer is passed into this function, an invalid_argument exception will be thrown". That's fine, but you're changing the specification there. The point is that any time that the behaviour and the specification diverge, then you've got yourself buggy code, and should probably just halt before it does damage.

I agree that specification/behavior divergence is an error, in fact I've been stating this since the beginning.  The question isn't if that is an error, the question is what is the best course of action to take when an error occurs.  Except that its taken all these posts to get to the real question because you keep implying that it isn't an error because assertions aren't really errors and hence its not error handling.

Exceptions, return codes, assertions, are all just different tools to solve the same problem.  We should be debating the pros/cons of each tool.

And if ignoring an exception is bad, then how is automatic program termination any less bad?

An unhandled exception is just as bad as an assertion failure -- neither should occur in a well formed program. 
Exceptions get slugged here because they're being used for two purposes (recoverable errors and unrecoverable errors). If the program has detected that all of its assumptions are invalid and it's not behaving according to the specification, then it makes no sense to try to catch such an error (except as a "crash handler" that aids in shutdown / logging).

You're ignoring what has been stated many times now.  There are errors that under some circumstances may be unrecoverable, and are recoverable under others.  Assertions do not allow the user of the code to determine this.  Error codes and exceptions do.

I don't see how you can criticize exceptions and then turn around and tout the same problem as a 'feature'.  This is nonsense.

Because exceptions actually have an impact on your code. Writing code that provides the Strong exception safety guarantee is extremely hard in many cases. This burden is applied to your entire code-base (or ignored, leading to the possibility of class invariants being violated, which is a terrible choice to make). Having the program terminate when it detects that an invariant has been violated (and thus, the code is wrong) does not put any burden on the rest of your code-base. The fact that you think this is nonsense implies that you're not aware of the different exception safety guarantee levels and their respective complications.

This is a red herring.  I understand what exception safety guarantee's are.  Whether you are using assertions/error codes or exceptions, doesn't change exception safety.  You can't argue that terminating the program with an assertion is better from an exception safety standpoint than terminating the program from an uncaught exception.  And I haven't found that maintaining exception safety in a program (that isn't being terminated) is easier with error codes than with exceptions.  In fact most times you don't even need strong, rather weak is enough for the program to resume without undue duress.  In fact there are many times that even with a no-guarantee you can still recover some data; less of an issue with games but can be helpful with applications.

Now I know what some might say here, exception guarantee's only apply to exceptions, that's there's no exception guarantee's if there are no exceptions.  Not really.  Assertions/error codes give you (from https://en.wikipedia.org/wiki/Exception_safety) 1 and 4, which are very easy to do with or without exceptions (programs fine, or program's terminated, trivial).  2 and 3 are more nuanced but can be emulated by error codes (for example vector::at() could use an error code instead of an exception to indicate out of range, something like bool vector::at(T&), messy but theoretically possible).

The exceptions-everywhere crowd (*cough* Java) just like to move errors out of pre-conditions/invariants/post-conditions and move them into runtime checks.
The no-errors crowd like to rely on pre-conditions/invariants/post-conditions as much as possible to prove at build-time that errors cannot occur, and therefore don't require runtime checks.

No.  pre-conditions, post-conditions, and invariants are are all run time errors.  Sometimes they can be checked at compile time.  Just like any modern compiler performs constant folding/constant propagation, any modern compiler that supports them will often be able to optimize these checks; but they are still run time errors.  Whether from user input, hardware errors, networking errors, or otherwise, these pre-conditions, post-conditions, and invariants will be violated at run-time (even though the code is correct) and must be handled at run time.

Pretending that these errors do not occur at run time, or are not errors, is simply a semantic game that makes discussion of the pros/cons of error handling tedious.

Share this post


Link to post
Share on other sites

I'm going to have to disagree, just because you don't handle errors doesn't mean they don't exist (and that doesn't mean I don't support turning assertions off in the shipping build, but just because there shouldn't be errors, doesn't mean there aren't).  Choosing to terminate, ignoring errors, or simply logging them, are all error handling strategies.  I'm all up for discussion the pros and cons of each, but stating 'write error free code' while claiming that 'assertions are not errors but debugging tools' I feel is disingenuous.  Its just not an accurate statement in context of the OP.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now