C++ Error Handling: Why Use Eithers in Favor of Exceptions and Error-Codes
hackernoon.com
hackernoon.com
Meanwhile the open source project he links to has an implementation that is hundreds of lines of template magic that I gave up on interpreting.
I've dealt with codebases like this and I strongly recommend against using this kind of arcane template incantations in production. It becomes an impenetrable fog for newbies, and a morass for veterans with something to prove.
~Either() {
if(isLeft) {
leftValue.~L();
} else {
rightValue.~R();
}
}
Which does explicitly call the destructors. (Obviously, if you only were referring to his eight-line toy version of Either for the blog, please forget my comment.)[EDIT: re-reading your comment once more, I see you did indeed mean the blog implementation; please ignore this comment.]
[EDIT2: BTW, it seems the author is "affiliated" with that LoopPerfect/neither implementation.]
[1] https://github.com/LoopPerfect/neither/blob/master/neither/i...
If you know how to handle an error when it arises, then you shouldn't be using exceptions. If you're wrapping a try block around every piece of code that might throw an exception, then you're doing it wrong.
The technique described in this article is not an alternative to exceptions... unless you were using exceptions wrong in the first place.
EDIT: Replaced "any code" with "every piece of code."
CS student here, can you elaborate on this? What code would you wrap in a try block if not code that could throw an exception? Are you suggesting try blocks should only be used to insulate function calls we know to be unsafe?
C++ programs normally throw an exception when a C program would call exit() or abort().
So I would start by having one try block that surrounds your main() function with a catch-all block that prints some sort of error message before your program terminates. This is often all you need.
If a situation arises in which you can handle an exception without terminating your program, you can throw a try block in there too.
Exceptions automatically provide for you what you would write by hand in languages like C:
// The C way
int error_flag = EVERYTHING_IS_OK;
int checked_div (int numerator, int denominator, int *quotient)
{
if (denominator == 0)
return 1;
*quotient = numerator / denominator;
return 0;
}
int foo (int a, int b, int c)
{
int q;
int err = checked_div (a, b, &q);
if (err) {
error_flag = FOO_DIV0;
}
return q+c;
}
* // The exception way (notice that there's no try block)
int checked_div (int numerator, int denominator)
{
if (denominator == 0)
throw runtime_error ("Divide by zero!");
return quotient;
}
int foo (int a, int b, int c)
{
return checked_div (a, b) + c;
}
Clearly, if you don't use built-in exceptions, you have to manually simulate the exception mechanism. The most popular strategies for simulating exceptions involve some combination of systematically returning and checking error codes or systematically setting and checking global error flags. I've demonstrated both above. The linked article is an example of the first strategy.Returning error codes prevents functions from directly returning their results. The "eithers" strategy is an attempt to make this a little smoother, but it doesn't solve the problem that it makes your code weird. Global error flags have their own host of problems. If you fail to check for errors, havoc will ensue. Furthermore, multithreaded environments turn global error flags into a tangled web of deceit.
Nevertheless, these ad hoc strategies often suffice because the unusual nature of exceptional errors can prevent error handling code from clogging up your primary code path too badly. But even for simple situations, the compiler can do better than you can. Why add unnecessary complexity to your code?
You should write try/catch blocks only when you know how to cope with exceptional errors. Needless to say, they should be pretty rare. But lets say that when a particular invocation of foo() fails, we want to use the maximum integer value as our result. Here's how it looks:
// The C way
int bar (int a, int b, int c)
{
int result = foo (a, b, c);
if (error_flag != EVERYTHING_IS_OK) {
assert (error_flag == FOO_DIV0);
result = numeric_limits<int>::max();
error_flag = EVERYTHING_IS_OK;
}
return result;
}
* // The exception way
int bar (int a, int b, int c)
{
try {
return foo (a, b, c);
} catch (runtime_error)
return numeric_limits<int>::max();
}
}
Hmm... The exception version doesn't look that much simpler. After all, the assert() call will be compiled out of release builds. But C++ compilers can, and most modern ones do, generate code for the exception version as if it were written more like this: int bar (int a, int b, int c)
{
return foo (a, b, c);
}
// This is only called when a runtime_error exception is thrown.
int bar_catch (int a, int b, int c)
{
// return to the same place bar() returns to
return numeric_limits<int>::max();
}
That's right. Even in the presence of a try/catch block, the code for foo is generated without the try/catch block. The error handling code gets stored somewhere else so it won't take up valuable cache space. Of course, what the compiler generates doesn't look exactly like my example, but the effect is the same. And this strategy works even for more complicated functions. The compiler creates a non-optimized version of the function. Then, when an exception gets caught, the non-optimized function can simulate the state to resume from.This exception handling strategy is often referred to as the zero-cost exception mechanism. The latest versions of Clang, GCC, and 64-bit VC++ all implement some variation of it. For compilers that don't, there's a cost to adding try/catch blocks to your code. But it's still cheaper than the cost of your own error checking. Even if it tried, I don't know if a compiler could possibly implement exceptions in a way that's slower than a manual error handling approach.
There's a myth out there that exceptions add overhead or make the control flow of a program more difficult to reason about. Of course, this is the opposite of what they do. Even if you're working with C-like code that has no user-defined classes, switching to exceptions is a win. Exceptions enable some of the fundamental abstractions in C++. How else are you going to handle a memory allocation error in an implicitly-called copy constructor?
If you'd like to get a sense of just how perversely you need to contort the C++ language in order to deal with the absence of exceptions, look no further than Google's C++ style guide. Google doesn't use exceptions in their C++ code. This policy has ripple effects throughout their guidelines. Among other things, they forbid constructors from allocating resources.
https://google.github.io/styleguide/cppguide.html
Google's excuse for not using exceptions is that their existing exception-free code base is so large that it would take too much effort to convert it. But if they can interface C++ code with Java/Python/Go code, they should have no problem interfacing exception-free C++ with exception-full C++. I can't help but wonder if some of their engineers are confused about how exceptions work.
Anyway, if you don't work for Google, use C++ exceptions.
Wow. That turned into a rant (some of which was copy-pasted from something I wrote a while ago).
Unwinding the stack through native library or language boundaries is fraught with potential problems and language interop APIs have to be designed carefully round that sort of thing.
I like exceptions, because they are magic. While prototyping or for small utilities it's perfectly OK to "crash" on any error - nice error message can help then.
However I prefer when the final version handles all errors explicitly. I want to see it documented in code that error handling was not forgotten and it was deliberate to push certain errors back the stack. Of course it can work with documentation telling that this library never catches any errors, but it will put the burden on me. I also prefer to have a code, because documentation, more often then not, is lying.
Having written all that, I'm wondering how exceptions work with Python's "Explicit is better than implicit". I guess in case of exceptions this rule has lower priority than some other rule that priorities simple looking code. That would align with my use of Python - strictly for one off utils and prototypes.
Which is why I actually like Java's Checked Exceptions, at least for public-API purposes.
It's really the exact same principle as having a contract for functions' return-values.
I'm pretty sure it was proposed more than once in the standard committee.
Well then the C++ standard people are doing it wrong, because something as expected-to-fail as converting a string to a number throws an exception.
What am I supposed to do if I want to load a text file containing numbers and print out where it failed? Yep, I have to wrap every string-to-number conversion in a try-catch block.
Or in reality I make a wrapper function that does it. This is why exceptions are stupid - 80% of the time you want the context provided by the program counter in order to handle the error.
int string_to_int(const std::string& s, int def) { try { size_t pos = 0; int v = std::stoi(s, &pos); // Check that the number was parsed from the entire string (stoi ignores trailing garbage). if (pos == s.size()) return v; } catch (std::logic_error&) { // This occurs if there is no conversion (e.g. no digits) or the number doesn't fit in an int. } return def; }
I wouldn't say that they are doing it wrong. They throw an exception because they don't want to make assumptions about how you would want to handle the error.
What would you prefer the std::stoi() function to do?
Or in C++17, return `std::optional<int>`.
Furthermore, if you don't check the "success" flag, then your function would fail silently. The exception ensures that something reasonable happens (program termination) when the client does nothing to handle the error.
Exceptions are cleaner.
This programming style is very specific to the language. In another language, exception may be used more widely.
The upside is that the happy path (without exceptions) can be branch-free (or at least contain fewer branches than equivalent code handling errors), which is beneficial to performance (hot path stays hot).
Ah, that's simply because C++ exceptions are unwind-only, non-restartable.
"Blub" exceptions.
> This is why exceptions are stupid - 80% of the time you want the context provided by the program counter.
Exactly.
Imagine if a CPU exception threw away that context. We couldn't implement virtual memory, which requires faulting instructions to be precisely restarted.
Why would you want to restart same code after an exception anyway? Conditions have changed...
So I think exceptions are a good thing for unexpected outcomes; Resource errors, programming errors, etc. The problem is, we use them in cases where the error is, technically speaking, a possible, or "expect-able", outcome (such as the classical example of de-serializing data). In those cases, I fully agree, a type-level, explicit handler using the Either monad would be better.
I even think, the "expected case" is why Java introduced the notion of annotating exceptions. But as there are two different use-cases, this did not mix well...
Such a design forces you to handle all errors near the place they occurred without the syntactic overhead. (Threading problems are overrated. Easy to dodge by providing a Context attribute which then works like IO monad in Haskell.)
It essentially works like exceptions without a forced stack unwind and return - which may or may not be good. Both are easy to convert and interoperate.
What I learned working on a medium-sized web project is that you have too few exception types. Yes, create more unique exceptions for each situation and avoid reusing them across the project.
Too general exceptions make your code base suck hard. Complexity of exceptions with small benefit because you can't discriminate between them. ObjectNotFoundException is pretty useless but AccountNotFoundException is much better. Don't be afraid of types, use it to your advantage.
Optimize the happy path and fail fast on the sad path.
Note, have not tried this approach on a C++ project yet, maybe it will not work as well there.
If anything I would be very please if we got advanced exception catchers in C++. Recently I drooled over Ada exception handling capabilities: https://en.wikibooks.org/wiki/Ada_Programming/Exceptions#Exc...
Also a remark: the author uses left()/right() calls excplicitly to construct Either instances. This should not be needed (given Either has the proper constructors). One could argue it is clearer, on the other hand one could say it adds unnecessary visual noise. I'm obviously in the latter camp else I wouldn't make this remark :]
[1] http://www.llvm.org/docs/doxygen/html/classllvm_1_1ErrorOr.h...
It works well when combined with the some usual techniques like the error singletons and exceptions. You can also set an error code you need by capturing the result variable. Or just do what you need in the handler itself.
Returning a special type is about as annoying as returning and handling an error code.
- Callback-oriented programming is its own deep can of worms. What do you want to inline where? In case of leftMap, it's easy for a compiler to inline flatMap, because it's small. What if you're providing a lambda to a huge function? What about variable captures? Really, we've been there with JavaScript.
- It will either incur a yuuge runtime overhead or will require a new calling convention.
- What if you want to transform your data multiple times? Will you use nested lambdas?
IMO, using sum types is the best solution to this problem. Sum types are not "weird special type", they don't require "weird calling convention", C++ is weird for NOT supporting them.
What runtime overhead, a function call? Pointer dereference? Even template magic on the error monad type won't save you from that one. The problem is shifted to the dereference. There you get either to handle an exception or error code, or explicitly check the state of the object.
Nested lambdas do work reasonably well.
But remember, were generally talking about error handling, not a callback that is supposed to run big transforms.
At worst the overhead is a function call anyway and inhibition of inlining in the error path. Generally nothing to worry about.
But not in the standard library. Of course, in C++, you can do anything if you venture outside the standard.
Additionally, when using Either the lambdas are in the caller and are invoked almost immediately by map. The scope is small and they do not enter into any other compilation unit. If you pass them into the called function, the compiler is only going to be able to optimize them if the called function can be inlined. Even then, the optimizer will have to be smarter, because the error handler will travel through a much larger scope and a type conversion.
The pointer dereference and call overhead is not big and error handling code is supposed to be cold anyway.
So below is my take on this with 3 clear labels and guidelines on how to apply. Hope this helps.
There are 3 kinds of "errors".
1. Bugs.
- created by programmer.
- invalid state of the application - >it has transcended it's own logical realm and you can't reason about its behaviour anymore.
- null pointers, OOB, etc.
2. Errors that are "expected" part of the program execution.
- you need to write logic flow to deal with this
- it's expected and quite normal that this might happen
- incorrect (user) input, file not found, socket timed out etc.
- this is really just an error from the end user's perspective.
3. Errors that are "unexpected".
- some very unexpected error
- system resource allocation failed, out of memory, out of file handles etc.
- critical resource was not accessible (for example a "must have" config file was not found)
How to deal with these?
1. Abort and dump core. Yep seriously, just do it. Blow up with a bang and leave a stack trace that you can analyze in the post-mortem debugger and see what went wrong. As a result your application will be simpler and more straightforward. Simply, don't try to write logic to deal with programmer failures. It will just clutter your program, mask the problem and make fixing it harder.
int divide(int x, int y) {
if (y == 0) {
throw std::string("Divide by zero");
}
return x / y;
}
passing y=0 the function is clearly a bug. It'd be much better to core dump here. Unless the function was designed with double purpose of validating the input and performing the actual function. I'd much rather split these into two different functions, since the core function may be called from different contexts, some of which might not require any input validation at all even. And the validation function clearly should not be using exceptions either since it's quite normal and expected that input from external sources (such as the user) can be malformed and you probably want to write logic that then bashes (sorry.. informs) the user about their wrong input.2. Use error codes.
3. Use exceptions.
Figure out what the remaining viable state of the program is, report the error (yes, loudly!), and recover. This would be the far more correct advice for #1 (except for tiny binaries doing a single job only, as said).
Seriously, in some cases when you're going for example OOB, the only reasonable action is to dump core.
Trust me. This will improve your program, not make it worse.
Trust me, you don't want to bring down the whole house just to fix a pipe...
Ultimately high availability (in this context) is about making your software correct. When your software is correct you won't have availability issues because of bugs.
Meanwhile if your software does crash (or terminate in a controlled fashion) but you need high availability you'll need to restart it automatically.
But handling cases from #1 as you described is nonviable in (virtually) all cases (except as described: if the error prevents the binary from completing that single job it was run on). Just imagine any of the programs you are running on your machine right now would just crash because they are not resilient to an off-by-one fencing error or whatever other bug (and there will be issues, even in your three-liners). I bet you'd be pretty pissed off if your programs would crash on you every other day or so...
Whats the point of a program producing incorrect results? Whats the value of that?
Let's have an example.
Person& findPerson(key_t key) {...}
Now this is a core function, the basic precondition of the function is that the key is valid and exists.
The function is called with a key that doesn't meet the precondition. A programmer who wrote the client code has made a bug.How do you deal with this situation?
Throw and exception? Change the signature and return an error code + person? Great, now you've turned a bug into an "error", masking the actual problem. Then what does the calling code do? How is it prepared to handle "bugs". What does the logic to deal with bugs look like? Then you write bugs in your code trying to deal with bugs?
Alternatively (I know some big orgs do this..) they put in a log and then return some "default object" and pretend there was no problem. Now how's the calling codes computation going to go, it's given bogus data to deal with.
Bottom line, I want my programs to be bug free and correct. I don't want to even try to clutter my code with trying to write logic to deal with bugs.
What I want on every bug is clean termination, program state and stack trace. It makes it very easy to fix the bug thus actually improving my software quality.
I'm happy to know that my software actually has very few bugs.
I know that you might think that it's inconvenient for the user to have his software abort. But if the software produces corrupt results then whats the value of that? Even worse, the user can think that results are correct even though it's garbage.
Seriously, going back to the example. iF the precondition is validated you abort. Your developers can easily fix the offending code, release a new version of the software and your system is just that much better.
If you incorporate this methodology from the start of the project you can greatly improve your quality in general.
Second, you did not address my main argument, that anybody (including you, your code's customers/users, etc.) would be very annoyed if our software were to crash on every remaining issue in our programs. And that argument goes as far as that the behavior you suggest could lead to us not using that service, software, OS, or even computers as a whole.
This is typical for modern mobile applications which are supposed to be killable and restartable at any time.
In other words, make application idempotent for any start condition.
And I'm not saying it will necessarily lead to fewer bugs. What I'm really saying is that allows one to debug identify and fix the bugs easily, or easier than some other alternatives such as printing some error message to some log and then (possibly) continuing in UB land while silently corrupting state and then finally causing some new weird error (or crash if you're lucky).
So what I'm really advocating that dumping the core gives you two benefits:
- simplifies your code when you don't try to write logic to deal with broken logic itself.
- simplifies bug detection, analysis and fixing.
Together these two translate to more correct programs assuming that you actually go and fix the problems
Also you didn't answer my question. I'd like to see what kind of kludges you'd recommend.
I don't follow, for me it's not clear at all. What if y is user provided? Then the argument should be checked before calling your function. Then it's checked again inside the function.
Well, the bug here is that user input wasn't checked.
Then it's checked again inside the function
Checking twice is better than never checking at all (i.e. in the case argument validation didn't happen outside of the functiion)
It will just result in your minefield function being wrapped inside safe variant which just returns an error in some form. Which could happen in the first place.
Seriously the best thing is throwing an exception on the bug path too. If it's truly a bug, then it shouldn't be handled and you get what you just described.
If it's a bug that could be constrained into a subsystem, you can maybe get away buy bringing down the subsystem only. That's what actually happens with your strategy too, but on the process/OS/service hypervisor level.
Except that you don't get a call stack (and process state), which is like the best feature of a core dump. Just open it in your debugger and you're immediately looking at a whole lot of more of information about what happened than what an exception can tell you.
In case 1 we usually use asserts though (which are always on), but that's more of a preference because it states intent (asserts are just used to guard against insane programmer behaviour, and they are used judiciously, to the point we will refuse large patches which do not introduce any asserts) and makes debugging easy (since they have the debugger break). As a result, code became less buggy. Programmers don't want to trigger asserts because they are 'crashes' and being the cause of one is usually the thing you don't want to be. As such, people become more aware of every single line of code written, every input provided, ... . Yeah there might be other ways to reach that effect, but using assert all over the place did the job just fine.
Works out fantastic.
Exceptions are to error codes as functions are to goto: there are valid cases where you want to prefer the latter, but they are few and far between.
This approach is used in iostream for example, and in a broken way in C library via errno. (Which does not work unless you check it very near.)
Similar approach is often used in databases and file systems, marking an object as dirty or broken.
Like the above, it is prone to ignoring errors and attempting to manipulate such object.
A bonus of this approach is that error path through code is the same as for the non-error case. Thus getting good coverage for error cases in unit and integration tests is easier.
What surprised me about this link is that the author was writing C++, but using very Haskellish language and terminology. He even made the C++ code look somewhat like Haskell code. Wow.
Note that in c++, the absence of algebraic data type made it a little bit hard to understand how Either is coded.
Otherwise no, there is really no good reason for having some orthogonal value returning system that can jump up the stack until it's caught (if ever.)
Many situations that are often considered exceptional are really not: cannot connect to server, no such file or directory, cannot bind to port...
Including such connection failures or file open failures. They may have to be handled, but not at cost to the hot path.
You cannot typically just "eat" such an error with default behaviour and expect whatever relied on it to work properly.