5 ms·
This seems to be a failure to understand when exceptions and when error codes are to be used. I'm just starting a big infrastructure project that needs to be hi
by KayEss 11y ago
This seems to be a failure to understand when exceptions and when error codes are to be used. I'm just starting a big infrastructure project that needs to be high uptime and distributed across a large number of machines, and I'm doing it in C++ because I know I'd never get it working in anything else.
Let's take one specific example -- connecting to a remote host. The loop that goes through the DNS returned IP numbers uses local error codes to try each in turn until one is accepted. If none are accepted it throws an exception so that the higher level application code can decide what to do about the connect failure. The exception ensures that all resources are properly cleaned on the way through.
The exception is used to guarantee that resources are properly de-allocated on the way back up to where the error needs to handled as it cannot be handled locally, probably not in the function that kicked off the connect attempt either, but there's no local exceptions to handle the case where the error is handled locally.
The use of the exception is more like a ROLLBACK on a database transaction together with error reporting -- it helps ensure correctness by backing out properly changes in state that were kicked off by the connect attempt. When the exception is caught the program state is exactly as it was when the connect attempt was first tried so we know we have good clean state to make another attempt or to move on and do something else. Backing out all of these other state changes is really hard when an error code needs to be handled non-locally -- and it's this non-local handling of errors that's so hard and error prone without exceptions.
So if you're only ever using error codes you'll far too easily make mistakes when the error needs to be handled non-locally, and if you're only ever using exceptions then it's going to be real ugly when the error is handled locally.
You need to use both if you want clean code that is going to work reliably (or you need massive engineering resources to fix all of the bugs you'll end up with).
- bjourne 11y agoI think you are right about the author not understanding exception handling very well. But your handling of exceptions is also incorrect. In a fault-tolerant system, dns lookup failures or repeated connection attempt failures is something you design for -- they are not exceptional events. E.g you have function called connect() then it should return something like a state object with a connection instance or an error indicator such as {err:dns_fail, dns_servers:[..],hostname:".."} Otoh, if you were writing a one-of script for scraping a web site then you don't care about fault tolerance and then connection failures are exceptional events so throwing on them becomes ok.
- KayEss 11y agoI disagree -- the determinant is about state management and locality of error handling and this is all about how your code is structured, not whether the error is expected or not in some nebulous sense. If you can handle the connect failure locally (i.e. probably no further away then caller of the connect function) then by all means do so. If you find another structure in your code makes other things simpler, but no longer allows you locality of error handling you should use an exception. Well designed libraries allow you to choose. Check out Boost.ASIO for an example of this. Every API has both an exception and error handling version and you're free to use whichever is most appropriate for what you're trying to achieve. This whole thing about 'expected' and 'unexpected' errors is a red herring that leads people down the wrong path. The exception backs you out of the transaction that your code is performing -- this is the way to think about it. An error code allows you to try something else whilst keeping the transaction alive. Which you use depends not at all on whether you think the error is "normal" or not.
- bjourne 11y agoWell, the reason exceptions should be reserved for exceptional cases is because they are gotos. There is no way around that, a function that both returns values and throws exceptions is more complex than one that doesn't because you are jumping through call frames. I once worked with a billing system that threw a BillingException when a charge didn't go through. It works ok, as long as your only response to such an error is to spew an error message to the user. But the more you need to handle it, the less an exception makes sense. The code that tried to handle the BillingException was a tangled mess of try and catch statemetns mixed with retry loops. For example, if there was a temporary error at the payment provider, you would just retry a few times, a permanent error, try with another account, if the customers remaining funds were to low show an error message or if they were just a few dollars of, charge them that and be happy we got something from them. On the other hand, the billing system could also throw a DbException in case there was something wrong with the database connection. That's a different kind of problem and fatal for a database-backed website. Nothing to do, except log the error and crash. The point of exceptions is not so much that you can catch and handle them, but that it separates exceptional situations from in-band normal error handling. Now what is in-band and what is exceptional depends on the circumstances which is why, as you say, Boost.ASIO provides both variants. If your system is supposed to handle the errors, use the one without exceptions. If not then use the one with exceptions.
- mathgenius 11y agoYeah, it's a nice example. However, I disagree on the severity of your prognosis. In C (my preference over C++) I would use an arena memory allocation strategy in this case. This is something I learned from reading the python interpreter source code: when your parser barfs way down in the call stack, an arena is a simple way to clean up everything. (I'm not saying it will be easy in your particular case.) As for exceptions, you can just use the return value to signal an exception, or if you want to be a bit more radical, use a longjmp.
- KayEss 11y agoThe slab allocator is a great strategy, but of course only works for memory, which is really a special case of one out of all of the resources we want to manage. If you're writing long running processes you need to care about any number of file descriptors, maybe mutexes, certainly locks. You can't bypass all of this in all cases assuming that you only need to deallocate memory. Destructors tidying this up as the exception unwinds the stack is extremely easy to reason about and stops any number of idiotic mistakes.
- helmut_hed 11y agoFor anyone who is curious about the intended use of exceptions in C++ I recommend Jon Kalb's two-part tutorial on exceptions from CppCon 2014. First video is here: https://www.youtube.com/watch?v=W7fIy_54y-w https://www.youtube.com/watch?v=W7fIy_54y-w