8 ms·
Why do we need to have ambient control flow? This is what exception handling is, it's a hidden control flow. I don't use them. I just create an error type and
by john567 5y ago
Why do we need to have ambient control flow? This is what exception handling is, it's a hidden control flow.
I don't use them. I just create an error type and pass that around.
The only legitimate exception I will accept is when you access invalid memory. That's a special case and depending on the environment something extraordinary must happen.
But exceptions and exception handling just creates annoying code. It doesn't add value, not really.
- jdlshore 5y agoThere are some kinds of errors that can’t be handled locally, but do need to be handled globally, or generically higher in the call chain. Continuing execution after the error occurs will make the problem worse. Exceptions allow you to cease execution without putting an if statement after every function call.
- jeffbee 5y agoThat's what you get from disabling exceptions: a call to std::abort instead of a throw.
- InfiniteRand 5y agoIn some ways that’s throwing an exception into the calling environment in the form of an error code
- WkndTriathlete 5y agoHaskell's IO monad says, "Hi! With me you don't need exceptions and you don't need to put an if statement after every function call."
- gpderetta 5y agoHow is the implicit control flow in do notation different from exceptions? If anything, the issue is between checked and unchecked exceptions.
- mibsl 5y agoHaskell's IO monad actually has excellent exception support, including async exceptions and masking them in critical sections. There are `Maybe` and `Either` and they're great at streamlining error handling in pure code, but when it comes to IO most libraries just throw exceptions (including the standard library).
- wvenable 5y agoExceptions come naturally from the realization that you mostly have to propagate errors to where they can be suitably handled or logged. And all that propagation code heavily detracts from the meaning of the code when you are writing it or reading it. And messing up the propagation is a common source of issues (historically). With "exceptional" errors are are only 2 real recovery options: restart the operation or terminate the operation. Neither of these are typically decided on anywhere where an error might occur in the call stack.
- mort96 5y agoI also have to imagine there's a decent performance boost. With Rust's and Go's (and C's, usually) approach of having 'if (error) { return error; }' all over the place (with syntax sugar or not), there will be a lot of extra branches in the happy path. Sure, those branches are predictable and thus fast, but they're not instant, and they take up icache space in the happy path. Modern exception implementations can almost exclusively slow down the exceptional code, and code which propagates exceptions without catching or throwing will look identical to code with no error handling at all. I'm sure the gains aren't tremendous, but lots of C++ design decisions are for slight performance improvements at the cost of less safety. Other examples are unchecked array access by default and unchecked overflow. Whether these are the right decisions or not is debatable, but at least it's consistent. EDIT: Of course, as the article points out, if you have a high error frequency and many cores, exceptions cause significant performance issues. But in the case where exceptions are _actually_ exceptional, and especially in cases where the only real response to an exception is to log an error and exit, exceptions are exceptionally good (pardon the pun) from a performance perspective.
- ithkuil 5y agoNot all errors are "exceptional". For example, consider an API that lets you open a file (local or remote); it may be very common that a file doesn't exist and it may be natural to handle that case by checking a "file not found" error (checking if the file exists before accessing it incurs in an extra cost and it's also racy).
- jstimpfle 5y ago> Exceptions come naturally from the realization that you mostly have to propagate errors to where they can be suitably handled or logged. I agree of course that errors must be handled and logged as appropriate, but the implication that this has to happen by popping from the call stack is not justified at all.
- masklinn 5y agoOne of the core problems is C++’s design basically requires it: there’s no other way to error from a ctor, and since ctors are used as hooks in many operations the dishonest rejoinder of “just use a factory” doesn’t work in any capacity.
- mikepurvis 5y agoDoesn’t some of the reliance on constructors for everything come from how constness is fetishized in C++? Not that it's all bad to have those checks in place, but Python doesn't have this issue with out of control constructors in part because (almost) everything in Python is just unapologetically mutable.
- chippiewill 5y agoYou could potentially argue that, but certainly Rust doesn't encounter this issue despite being const by default because they simply don't have constructors in the first place.
- mikepurvis 5y agoYeah I knew I was going to get called out with a Rust comparison. I think the Rust approach basically acknowledges the issue with what C++ did— that automatic initialization is cute but ultimately wasn't worth what it ended up costing in terms of hidden control flow, poor error handling, static initialization issues, etc. Anyway, Rust basically deals with it by giving the class designer the choice to supply factory functions or punt on it, making the user initialize every field themselves each time. And I think most agree that this is a good approach; it's the best of C++ (factories) with a better fallback than a default constructor.
- gpderetta 5y agoBut you have exactly the same options in C++.
- 5y ago
- CyberRabbi 5y ago> The only legitimate exception I will accept is when you access invalid memory. If all memory accesses were done with a function call, e.g. read(void *addr), then by your logic there would be no legitimate need for exceptions. If you extrapolate into the other direction, exceptions are convenient from a syntactic POV because it avoids littering every operation with an explicit error return mechanism.
- edflsafoiewq 5y agoMy problems with Result/expected/etc 1. They generate syntactic noise at every point they touch the call graph: function signatures, calls, returns. 2. In particular, if a change causes a deeply nested function that used to always succeed to be able to error, the entire path up the call graph needs to get Resultified. 3. Since the caller must be aware of them, generic code generally has to be Result-aware too. 4. They aren't a total solution, exceptions usually have to exist anyway (eg panic), for things like oom/assert/etc. So you're usually paying the cost of them anyway. 5. You only get what the callee gives you. With exceptions, you can get a backtrace to the cause of the error by default, with no effort needed on the part of the callee.
- zozbot234 5y ago> They generate syntactic noise at every point they touch the call graph: function signatures, calls, returns. This is not "noise" but needed information. A function that can error out should not have the same signature as one that will never return an error. Similarly, call-site special syntax (like '?' in Rust) helps address the concerns raised by hidden control flow. > Since the caller must be aware of them, generic code generally has to be Result-aware too. True, but the Rust standard library includes a zero type that can be used to mark a Result-aware function as infallible, making it easy to wrap it with a non-Result type.
- stickfigure 5y ago> This is not "noise" but needed information. That is incredibly domain-dependent. In most general business processing, that information is just noise. Building a web app? 99.9% of the time, you let exceptions get caught by the http server and return 500 to the client. In a few rare cases where you want to do something else, you catch. If you don't catch, the client still gets 500 - a perfectly acceptable fallback. Building a GUI app? 99.9% of the time, exceptions in the UI loop should just display an error message to the user in a modal dialog and then get ignored. Sure, you can do something else, but the error dialog is a reasonable fallback. There is no good reason to torture the whole call stack to accommodate these problem domains.
- 5y ago
- mjw1007 5y agoOne of the motivating examples for exceptions, at the time when they were beginning to appear in mainstream languages like Ada, was to allow people to write arithmetic expressions using familiar notation, while still having a place to put an error handler for the overflow case. Perhaps the lesson of the last forty years or so is that this convenience wasn't worth adding such a heavyweight feature to the language, but it seems to me that modern languages are still weak at handling overflow.
- pjmlp 5y agoC++ is the only language where exceptions are such an ideology war. All the other ones that have born with exceptions don't have this issue, including Ada.
- masklinn 5y agoI'm not sure that's true. Exceptions are a big issue in Javascript, though mostly because they're absolutely terrible. Whether to use exceptions or not is a common question in e.g. C#, and several APIs are duplicated to have both exception-based and values-based variants. And Python is oft criticised for using exceptions more than once every blue moon.
- ncmncm 5y agoNo legitimate conclusions can be drawn from Java or C# experience, besides that it is best to stay well clear of them.
- pjmlp 5y agoIt is a big difference to offer both kinds of APIs, or to make endless flamewars with compiler runtime forks, which is what disabling exceptions and RTTI mean in practice, a fork from ISO C++.
- Koshkin 5y agoThere are only two kinds of languages: the ones people complain about and the ones nobody uses. ― Bjarne Stroustrup
- throw10920 5y ago> Why do we need to have ambient control flow? Because, properly-used, it saves you a lot of effort. Returning an error type unwinds the stack. If I have a low-level computation that has a number of error cases, and I want the high-level code to intelligently handle some of those error cases and continue processing, that's not doable using return values. Error codes bind the decision of which error-recovery strategy to take (which is only present at a high level) with the details of that strategy (which is only present at a low level). As a trivial, obviously-fake example - if I have a high-level GUI library that makes use of a low-level division function, I might occasionally divide by zero. Depending on what the GUI library was doing, I might want my divideBy(x, y) function to return 0, 1, the first argument, the second argument, or not return anything because that section of the code will be completely aborted. Without ambient control flow, if you just have return values, you have to check the return value for every single division operation you perform - and you'll have to re-implement code paths where a division operation failed. What if you have a long-running operation? If you return an error value when that operation happens, but it turns out the nature of the operation allows you to ignore that error and continue, you'll have to re-start the entire computation. If you hard-code the lower-level logic to ignore errors and continue, then you'll also end up ignoring errors that you really shouldn't. Error types do not solve these problems - in fact, they require that you duplicate lots of code to, say, make multiple variants of a library that are identical except when it comes to error-handling - or just cause your applications to lose lots of error-handling nuance and bail early on lots of exceptional circumstances that could be recovered from.
- zozbot234 5y ago> What if you have a long-running operation? If you return an error value when that operation happens, but it turns out the nature of the operation allows you to ignore that error and continue, you'll have to re-start the entire computation. This is just as much of a problem with exceptions. "Fix the error and continue" needs some equivalent to resumable conditions, which are generally implemented at the "low level" using coroutines. My understanding is that async-await as a language feature might be able to express these with relative ease, but exceptions alone clearly do not suffice.