Other people posting here have noted that using our language's type system intelligently is how we make trouble fail to compile. Counting on our and our colleagues' finely-tuned sense of rightness to keep out bad code is a recipe for having Bad Code. It suffices to explain Microsoft.
> I don’t like exceptions because they are, effectively, an invisible goto
This is supposes to invoke dread because everyone knows goto's are evil right? But if any jump to a different place in the code is a goto, then if/else/while are also goto's, and so are functions and returns.
I like Rusts "foo()?" syntax which explicitly indicates that the function call may exit with an error.
Control structures and function calls are a very statically-well-behaved control flow: unlike gotos, when you see a jump you know exactly where its destination is (runtime polymorphism for functions\methods is... an exception, and that's why some people don't like it.), unlike a goto where that might be a dynamic address. The Return is the only modern goto with a dynamic destination, but even then the destination is not completely arbitary (stack discipline), and actually the destination is irrelevant if the function is a well-named non-leaky abstraction, the function simply returns to its "caller", wherever it may be.
Furthermore, traditional control flow is Structured, it "owns" its jump labels. Nobody can jump to an else clause of a conditional except that conditional, nobody can jump to a loop start except the code immediately before it or the loop end (breaks and continues are, again, limited exceptions to this, and that's why some people don't like them), only the Returns of a function can jump back to the callsite of that function. As utterly trivial and basic as that sounds, it's a marvel. Gotos are nothing like this, they jump to public labels, anybody can jump to the label, it's a communism of control flow.
Exceptions break all of that. You don't statically know what's the destination of a Throw is, and unlike a Return, the destination is not irrelevant, the program is disrupted, you really need to know where that Throw is going to end up. Throws don't own their catches as well, any Throw can jump to the same Catch.
In a very real sense, Exceptions are more dangerous gotos than ifs and whiles.
The only choice to make is: here, or not here. If here is at a high enough level to do something intelligent about it, catch it. If not, it is not your problem: there is nothing to be done, so you do that.
Generally, a good program will have very few places where exceptions are caught, and the code there is easily exercised and well tested. The destructors that run on the way there are also well exercised because they run with or without exceptions. Beware code that only ever runs in exceptional cases. It grows bugs when what was correct becomes wrong as the code changes around it.
> unlike a Return, the destination is not irrelevant, the program is disrupted, you really need to know where that Throw is going to end up.
I don't follow your reasoning here. A function should not know where it returns to and a throw should not know where the exceptions will be caught. That is the point of those constructs. If you know exactly at the point of throwing how you want the exception to be handled, then you wouldn't need to throw an exception in the first place.
Exceptions are an entire call/return system hidden beneath the vanilla call/return system, with less thought in the design and more foot guns.
>I don't follow your reasoning here
Okay forget it, it was a somewhat subjective point, my main point is as above, exceptions are extremely spicy returns, they interact dangerously with the regular call/return system and scoping. That's essentially all of what objections to exceptions boil down to, and it's a legitimate reason to hate and avoid them.
FIFY.
In particular, destructors run, absolutely reliably. Rely on them. Using modern C++, you rarely need to code your own destructor; usually the language provides one that is guaranteed to be correct.
I don't get that. A function can have multiple returns and a return can return to multiple different places depending on where the function was called from. I don't see how they are "paired" or why that would even be a desirable property?
It is funny that people using Rust would code almost identically if the language had exceptions. Its lack might end up what keeps Rust performance from catching up to and passing C++. The compiler might come to quietly use exception machinery for most call chains, as an optimization. That seems hard, but a 15% performance boost is worth a lot of work.
If you want even more kickass bragging rights, you can program in a Lambda Calculus, where every single thing you try to do with the language will force you to think about it, more thinking ! yaaay ?.
Generally speaking, yes, the entirety of good design (in general, in all walks of life) and modern PL research comes down to "don't make me think about how I code", because good programmers don't think about the code, they think about the problem the code is trying to solve. As per Alfred Whitehead (a mathematician, so very much a fan of thinking) : "Civilization advances by extending the number of important operations which we can perform without thinking about them.", so is programming language research. It advances by allowing you to forget as much as possible that you're even programming a computer, a good language fades into the background, ideally you're not even programming a good language, you're simply stating the problem to be solved.
This is hampered by leaky and unreliable abstractions like Exceptions. They are a step backward in Error Handling, no static gaurantees, no static type checking, aweful synchronization between use sites.
You demonstrate only that you have no understanding of exceptions and their use.
It is allowed not to understand things. All of us don't understand most things. Your mistake is in pontificating about what you wholly fail to grasp.
If you're insecure enough that my different opinions about what good code is and how it should be written leads you to claim that I don't understand things without evidence, remove yourself from the conversation right now. It's much better for you.
Call/Return Pairs are balanced at runtime, not lexically. From all possible returns in a function, only a single one runs, and terminates the whole function. Every call site is paired, at runtime, with a single return that will return control to it, and the entire set of candidate returns are known at compile time, and the compiler checks all of this to report dumb mistakes.
This is not how Exceptions work, you can throw more than you can catch, and you can catch more than what can ever be thrown (this is generally harmless, just garbage code that never runs). Neither the number nor the types of throw/catch sites are kept in sync statically.
>a return can return to multiple different places depending on where the function was called from
Every single call site of a function is paired with all the returns of this function. The returns of a different function are paired with the call sites of that function, not the first. Unlike Exceptions, where every catch is a jump destination for every single throw of the same (dynamic) type across every single function beneath the catch on the call stack. That's literally an exponentially larger set than set of all the returns of a single function.
>why that would even be a desirable property
It's desirable because it gives static gaurantees.
Given a throw, you can't even know how much of the stack it would unwind, it can potentially unwind the entire stack if no compatible catch is above it on the call stack*. Exceptions can break through their abstraction boundaries and unwind the stack of a calling component that doesn't know about them. Exceptions are fundamentally dynamically typed Returns that you can forget about. That's as big a footgun as you can get without being deliberately a troll.
Given a catch, you can't even know where is all the places that can go to that catch (unlike a call site). Oh, there is the trivial answer of course : Every single function called in the try{...}, and every single function that those functions call, etc etc etc. Again, an intractable thing to reason about. So people don't, but they have to, the throw/catch pair is a single logical feature, their uses must be reasoned about and kept in sync in assumptions and consequences.
None of this is how a return works. If Structured Programming advocates saw Exceptions, they would be horrified. Even C's gotos don't allow you to jump out of a function.
* : (which, of course, is a dynamic property: in "if(...) then {Do_Something() catch(){...}} else{...}", only one branch will actually catch, one branch can crash the program, and none of this is visible to the compiler at write time)
Agreed. Unfortunately Java and in some cases C# tend to encourage this because of the behaviour of library functions, e.g. those that parse numbers and/or dates (a string not being in a parseable format rarely justifies being considered an exception, at least at a library level).
On the other hand as an advocate of structured programming I find code that's full of ifs and multiple return statements and global error state variables to handle truly exceptional conditions where code is unable to carry out its primary function to be horrifying.
The arguments I give is for why Throw/Catch is a terrible control flow construct, a throw is an extremely dynamic goto that searches the call stack for its label, not guaranteed to be there by the way, and a catch is an extremely dynamic call that can resume control from any point underneath in the call stack. I don't know about you, but sounds to me like a disaster waiting to happen. Java tried to tame it by trying to incorporate exceptions into the type system, but it's half-baked and also comes with a big glaring hole called Unchecked Exceptions.
Errors or no errors, a bad control flow construct is a bad control flow construct. Error control flow deserve the same treatment as regular one.
>I find code that's full of ifs and multiple return statements and global error state variables to handle truly exceptional conditions
Two wrongs don't make a right. Ad-hoc error handling is awful, but so is Ad-hoc exception handling as well. There is plenty of error handling mechanism (either built-in to languages or as an architecture over boring code) that is not both.
(As far as regular vs exceptional flow goes, think about what sort of acceptance criteria are written into story requirements. I've never seen the expected behaviour for out of memory or even critical network failures written up as part of a user story. In many cases it's decided for you by the libraries used anyway)
First one call matches exactly one return. Then the same call matches a bunch of returns. Which is it?
Then, a catch catches innumerable throws? When I throw an exception, it is only ever caught exactly once. One throw, one catch. Unwinding an unknown number of call stack frames, on the way, is the whole point of exceptions. You don't get to count that as a flaw.
Exceptions are not, in fact, dynamically typed. Each one thrown has exactly one type, its whole life long. All throws from that place will have exactly that type, and none other. Any catch block that doesn't lexically match that type is skipped over.
Look up "exponentially". It does not mean what you seem to imagine -- if in fact you can be said to mean anything at all.
Spilling out your entire mess of incomprehension does not only make you look silly, it is also rude.
>First one call matches exactly one return. Then the same call matches a bunch of returns. Which is it?
There is usually a very basic distinction in Programming between 2 things : the runtime and the compile time. At compile time, the expression 5/0 is a perfectly valid integer expression, it returns an integer that can be used in all subsequent calculations. At runtime, it's a hardware trap, and the entire program will probably abort because of it. Integer or hardware trap? Which one is it? Both.
At compile time, a call site has a finite (and obvious) set of returns it's associated with, usually a relatively small set. At runtime, however, a single return out of this set ends up returning control to the call site. So a call site is associated at compile time with a small set of very obvious returns, and at runtime with a single return drawn from this set. Anyone who doesn't understand this probably hasn't programmed enough time to have a worthwhile opinion.
>Then, a catch catches innumerable throws?
Yes indeed. Every single type-compatible exception that can possibly be thrown on the call stack beneath a catch is catchable by it, this is, contrary to what you imagine, an exponential set, and I do really mean exponential, yes:
If 2 function are called in a try{} block, and each of those 2 function then calls 2 function, and each of those last 2 function then calls another 2 function, then the set of possible throws catchable by your original catch is all throws in the 8 functions. Catch candidate sets are exponential in the number of function calls in the try{} block they catch. Unlike an ordinary call site that can resume control from a very small set of known locations, a catch can resume control from an huge and intractable set of jumps.
>it is only ever caught exactly once
Actually, it's at most once.
>You don't get to count that as a flaw
I do and I did. Exceptions are not some family members of yours to be this defensive and upset because I criticized them. It's okay, people can hate things that you (unhealthily) love, learn to deal with it. It gets better.
>Exceptions are not, in fact, dynamically typed.
This was a subtle point in my comment, so of course you didn't understand it and went on to angry ranting as usual. Here's a more detailed explanation of what I meant by that:
When you call a function foo() and assign the result it returns to an int, the compiler of any decent language will ensure that every single return in foo returns an int. Alternatively and equivalently, if you declare foo() as an int, every single variable that holds the result of foo is an int, else compiler complains.
This doesn't happen in exceptions.
In exceptions, you can throw a FileNotFoundException, in a function that only catches NetworkTimeoutException. The compiler won't complain. You can later call that function in a function that only catches DictKeyNotFoundException, and the compiler won't complain about the lack of catch for FileNotFoundException. You can keep doing this till the very end, forever in fact, always putting incompatible catches around the function that throws, and the compiler will never once complain.
It's in this sense that throwing exceptions is like a dynamically typed returns, it throws an object, i.e returns data, but the compiler never bothers to check for a matching reception of this object at the calling code, like it does for ordinary returns.
Java tried to solve this with its Checked Exceptions, but it also introduced Unchecked Exceptions that anybody can use and thus greatly reduced the benefit and use of Checked Exceptions.
>Look up "exponentially"
Try thinking about something for 5 minutes before you hit the keyboard. It will payoff greatly.
>Any catch block that doesn't lexically match that type is skipped over.
Look up "lexically". It does not mean what you seem to imagine.
A "lexical" thing, in programming language theory, is that which can be inferred from program text alone without running it. Catches don't work lexically because they respect subtyping (i.e inheritance). And with subtyping comes runtime polymorphism.
Specifically, if
- Cat and Dog are both subtypes of Animal, an abstract class\interface, and
- I'm catching a Cat,
Then if I throw an Animal exception, the question "Will my catch, catch that exception?" can't be resolved lexically, or from program text alone without running it. The catch will catch if the object pointed to by the Animal reference I threw is a Cat, it won't catch if the object pointed to by the Animal reference I threw is a Dog. Seriously, go run it and tell me if I'm wrong.
Again, that is allowed. But lecturing in such a condition is foolish. Worse, it wastes the time of any readers who might look for sense in what you posted, which is rude.
No, that is the entire point of exceptions. They unwind the stack up until the level of a matching catch block, if any. Why would you want to know how many stack levels a throw will unwind? When you call a function you don't know how many functions it will call in turn, and when you return a value you don't know if the callee will return it another level up. You shouldn't want to, since this would create and undesired tight coupling which would make the program hard to modify. It's a feature, not a bug.
> If Structured Programming advocates saw Exceptions, they would be horrified.
I'm sure they would, just as they would be horrified by having multiple returns in the same function - never mind horrors like "break" and "continue". The dogma was "single entry single exit". Exceptions does adhere to the "only lexical blocks and function calls" paradigm, but throwing is a form of multiple return, so it breaks with the dogmatic structured paradigm. But I prefer simple and maintainable code to blind dogma.
> Even C's gotos don't allow you to jump out of a function.
Goto actually has its uses in C, for example for resource cleanup. I think "goto harmful" have become a mantra where people use the words but forget the underlying reasoning. Dijkstras problem with goto was not that they jumped to a different place in the progam, his problems was that they didn't adhere to lexical structures and call stack. Exceptions actually allow resource cleanup in a structured way (using "finally" blocks).
Which is why they're bad design. The comparison to function calls is misleading :
- Function calls are named, given a function name you can always go to a function definition and see for yourself how many functions it calls in all return paths. You don't have to, but you can, and this is crucial in many many situations, not the least of which is debugging (e.g "oh no! program ran into infinite loop, What was the last function called? I will go and see its definition")
- Function calls can be statically type-checked, given a function that returns X type, the compiler will ensure that every reference that holds its return result is X or one of its subtypes.
But in exceptions :
- Catches (the equivalent of calls) are unnamed, the set of places a catch can catch from is exponential in the number of function calls in its try{...} block. For an example, if you want to debug given a catch, you must recursively inspect every single function call in the try{...} block to know which function threw the exception. And no, the 50-line stack traces don't make things any better, if any thing they just rub in the fact of how many functions you need to inspect to know where the problem came from.
- Throws are not statically type-checked, given a function that throws E1, the compiler will not bother to ensure that it is called in contexts where E1 is caught, you're completely on your own. It is this which makes Exceptions free program-crashing coupons. Only Java tried to solve this as far as I know, and the solution is still full of holes.
>I'm sure they would, just as they would be horrified by having multiple returns in the same function - never mind horrors like "break" and "continue".
You say this tongue-in-cheek, but as a matter of fact plenty of imperative languages ban "break" and "continue" for exactly this reasons, they are a huge source of loop bugs, and many style guide have problems with multiple returns and regulate them heavily if not banning them outright.
Regardless, the comparison is disingenuous, I just spent ~1000 words or so arguing why Exceptions are so utterly and fundamentally different from ordinary control flow. No return statement unwinds the stack arbitrarily, possibly to its very end, and with absolutely 0 static gaurantees.
If you think this is okay, we won't reach any useful consensus, and that's okay. But your original claim that Joel engages in faulty reasoning when he rejects exceptions as a form of goto is wrong, there is very good reasoning why exceptions are actually a much more dangerous goto than most modern goto, and plenty of much smarter people than you or me (or Joel) follow and accept this reasoning. Those people include those responsible for some of the most safety- and\or performance- critical software, like games and operating systems.
>Dijkstras problem with goto was not that they jumped to a different place in the progam,
I read Dijkstra's paper, and I'm pretty sure when I say his problem was in fact exactly that. Unrestricted gotos make control flow an arbitrarily-complex graph, the fundamental reason for this is that the jump labels are public, and exceptions share that in a way that nothing else does, their catches are public to every single throw beneath them in the call stack. Throws are gotos that search for their labels at runtime, possibly failing and crashing the program or unwinding it beyond recognition.
if (condition){
throw
} else {
<rest of code>
}
Rather than: if (condition) {
throw
}
<rest of code>
The justification, which I found in this presentation (https://youtu.be/SFv8Wm2HdNM) is that if you only looked at the block structures, could you tell that the rest of the code might never run?Moreover, if you have exceptions, any line of code containing a function call can potentially throw, so that the subsequent lines at the same level are not executed. (So you can’t see from the block structure if code is always executed anyway, when the language uses exceptions.) This is as if you’d factored out the guard clause into a separate function that you invoke at the location where you previously had the guard clause. In the if-then-else alternative, you can’t factor out the if-then separately from the else. The guard-clause style thus also affords more flexibility regarding code organization.
I do see what you mean with your second paragraph, though. It's a good argument against.
When programming with exceptions, it is the common case that a function consists of a sequence of steps each of which can fail and thus abort the function via an exception. Simple validation steps are not a special case in that respect.
Some of us use a convention of a blank line after a block that ends with return, throw, break, or continue, to save on indentation.
Of course that can help only if you are not splattering blank lines everywhere, communicating only that you think the reader doesn't deserve to see much of your code at once.