I feel there's a serious disagreement here with how I tend to look at things. I don't want to look line-by-line and ask what would happen in case of an exception. In most cases the answer would be: That would be bad, since the rest of the code would not get executed and the program would remain in an invalid state.
It's much easier to know that there will not be an exception. Simple sequential code, the way it was intended to be by the ancient gods. And handle errors manually where they could in fact occur. I reckon that for my own code, that is only once in a hundred lines or more. Or maybe even more (lines) than that, since I'll abort directly in the callee in many cases anyway, so the caller never gets a chance to see the error. But if there were any point in handling an error in the future, we can still switch to handling it later.
Now you can argue that you can do that with exceptions as well (handling errors directly either by returning an error code or by aborting) such that exceptions would never bubble up. But in that case what's the point in exception handling to begin with? It's just the same now, except that there's a lot of exceptions machinery required in the language.
Simple? Additions may overflow, printf may fail, etc. Usually code looks simple only because it ignores errors..
As for sequential, we're in the age of multiple cores and GPUs..
> Usually code looks simple only because it ignores errors..
No, it looks simple when the structure is right. There are other error handling strategies than "handle it now, or pop the stack frame and let your caller handle it now!". But with exceptions that's all the choice you are given (unless you convert them to regular data).
Since exceptions strongly discourage alternative error handling schemes, no wonder they lead to unmaintainable code as soon as a considerable amount of conditions actually should be handled, instead of just bailing out.
> As for sequential, we're in the age of multiple cores and GPUs..
What point are you trying to make? Obviously what I mean by sequential code here is that you can easily reason "if control flow reaches point A, then B will be executed, too".
> What point are you trying to make? Obviously what I mean by sequential code here is that you can easily reason "if control flow reaches point A, then B will be executed, too".
That goes out the door with any parallelism whatsoever, because control flow may reach point A in thread 1, but then thread 2 kills thread 1, and point B is never reached.
It's increasingly hard to bury ones head in the sand and pretend every machine is a single-CPU single-task system with no interrupts.
Out-of-order makes thread synchronization a little harder (when implementing lock-free algorithms for example), but that has nothing to do with exceptions.
But this is only ever possible if the program moved to an invalid state in the first place. Granted, sometimes there is no way around that, but usually it can and should be avoided.
I once implemented a C++ class where some member functions implemented the strong exception safety guarantee. I can tell you, yes it really is that hard. Even when you know certain variables are pointers and can't throw, you are tiptoeing around on razorblades.
At my day job we use exceptions in Java a lot, and it's a good abstraction for rolling back database transactions. But for non-transactional code it isn't the best.
If you're mentally comparing against the way error handling in C, your position is much more defensible. Error handling in C requires manually releasing resources in the correct order, and it's easy for a destructor to slip by in the wrong order, to get missed entirely, or for someone to use the wrong jump label. (I'm thinking about Linux kernel style code, here.) If I'm reviewing some error handling code in C, I might have to scan up to the top of the function and count all the resources which need to be released, then look down to the bottom to see that the right label is being jumped to. That's a pain.
In languages like Golang, error handling and resource cleanup are a bit more orthogonal. You can look at a Golang call which returns an error and ask, "Does this handle the error correctly?" Usually this only requires looking at three lines of code at a time, maybe four. Resources are just released with defer, except in unusual cases.
> Once you get in the habit, it's not hard to scan through a program line-by-line asking yourself "what would happen if an exception were thrown at this point?"
I guess I never got in the habit. Maybe you have better habits than I do, or maybe I'm just not "smart enough" to be a programmer, but I find this hard, so I choose to use languages which make this easier. I find it hard in Python, hard in JavaScript, hard in C++, and hard in C#. I'm really not even sure how to e.g. handle exceptions thrown by C++ constructor failures separately from other things later in the block, e.g., I want to do this:
try {
MyType x{args, args};
} catch (SomeException &e) {
...
}
x.DoSomething();
And I'm not sure how to even write this as functional C++ without doing something weird or maybe just saying "fuck it" and wrapping x in std::unique_ptr.> In fact, I'd wager that for anyone who's spent years working in any language with exceptions this quickly becomes second nature.
Based on what I see during code reviews, I don't agree. Raymond Chen has a good point... when I review someone's Golang code, checking for correct error handling is fast and easy, it's mostly mechanical, easy to target with lint, and I can immediately move on to looking at the code semantics. With, say, C# code, or especially Python code, there are too many cases where a method call slips inside or outside the try/catch block where it belongs.
I've also been bitten too many times by bugs in production where a function throws an exception, and we have to decode the stack trace to figure out what the error means or how the error is even possible. This is typically easier in programs written in Golang, at least in my experience.
The more syntactic sugar a language has, the more likely it is that I don't notice an error condition that is being handled incorrectly. Golang, with its simplicity, makes you do some extra typing up front but in my experience it pays for itself by going faster through code review and bug fixes.
This depends on the kind of code you're writing. If you're mostly sticking inside application memory and not doing, e.g., IO, IPC, etc, then the exception approach is better.
I'm not saying Golang is perfect by any means, but I find the way it does error handling to be a welcome reduction in my cognitive load.
A good error message might look like this:
Could not travel to Alpha Centauri:
warp drive initialization failed:
power coupling 397 is offline:
temperature out of range (temp = 150°C)
A bad stack trace might look like this: DeviceClient::Connect()
std::promise::something
<anonymous function>
DeviceSet::PowerOnAllDevices()
<anonymous function>
std::etcetera
std::etcetera<some_big_thing, more_params, allocator=std::allocator>
TravelCommand::Engage()
VoiceCommand::RunCommand()
Error: temperature out of range (temp = 150°C)
Stack traces are useful, but they're a poor substitute for good error messages. There's just too much context missing from raw stack traces (which file, which URL, which device) and too much noise (anonymous functions, higher-order functions, etc.) That, and a stack trace often requires reading the source code to interpret properly. That might not be possible or it might be outside the skill set of the person interpreting the error.Exceptions optimize for the case where the exception bubbles straight up to some handler, but I'd rather not have my errors work that way. It's incredibly common for it to make no sense to bubble an error up without some kind of annotation or additional context.
return errors.New("")
You have control over whether the error messages are good or bad, you just have to insert the right annotations where the critical pieces of context are known.Stack traces are not like this, you can't really control whether they are good or bad, at least not well. If you want to make your stack traces more informative what do you do? You can't really change them, they just reflect your call graph.
Golang optimizes for the case where you get good error messages. Exceptions optimize for the case where you don't care about adding context to the error and just want to bubble it upwards. On the balance of things, I prefer Golang's approach because most of the time, I don't want to see the stack trace.
[Edit: this was a reply to a comment you seem to have rewritten (I think for the worse) but I think the response still mostly works]
errors are "failure as data" (hence, message), and you get to write the flow (including handling and annotating the call stack)
vs
exceptions are "failure as flow" (hence, stack trace), and you get to write the data (including logging)
May very well be paraphrasing a well known maxim about closures and objects as errors are a poor man's exceptions and exceptions are a poor man's errors.
try {
MyType x{args, args};
x.DoSomething();
} catch (SomeException &e) {
...
}
You can't do much with the x if it didn't construct correctly, so you either need a strategy for a safe default value for x, or you need to bail entirely if an exception occurs.
https://blogs.msdn.microsoft.com/oldnewthing/20050114-00/?p=...
https://blogs.msdn.microsoft.com/oldnewthing/20040422-00/?p=...
While forceful, I don't accept the conclusion of Raymond's argument. I feel it may be true for programmers primarily trained and experienced in C, but is not true in general. As such, the argument reduces to "programmers who are not familiar with exceptions sometimes make mistakes when using them." One could say the same thing about pointers, dynamic typing, lambdas, macros, comprehensions, pass-by-reference, etc. Today, most programmers are familiar and comfortable with exceptions, having almost certainly been taught to use them in college and almost certainly using them on a daily basis for their entire professional career. They'll also have been taught to use a language-specific toolbox such as RAII or try/catch/finally to deal with any potential problems.
Once you get in the habit, it's not hard to scan through a program line-by-line asking yourself "what would happen if an exception were thrown at this point?" and to take appropriate precautions. In fact, I'd wager that for anyone who's spent years working in any language with exceptions this quickly becomes second nature. It's also an easy way to add value during peer code review. In my experience, writing correct code with exceptions is not "really hard," as Raymond argues, but rather no harder and a lot quicker that using explicit error handling. The reason it's a lot quicker is because "exit the function with an error status and do local cleanup of variables initialized so far" is by far the most common correct action. And for cases when its not, the try/catch/finally or other exception handling whatever is straight forward to write and no uglier than explicit error handling would have been. Of course, it's possible I think this because I've worked in languages with exceptions for most of my career. But I certainly don't observe that even the most junior programmers on my team create a ton of bugs that could be attributed to mishandled exceptions. In fact, I can only remember a handful of such bugs, mostly involving open database transactions or unhelpful error messages masking more detailed error messages.
A more solid objection to language support for exceptions can be made by pointing out all invisible code generated by the compiler to correctly handle exceptions. This can lead to significant code bloat and in some cases can negatively impact performance. Or by pointing out how unwise it is to be allocating additional Exception objects while dealing with out of memory errors. These are some of the reasons why C++ isn't appropriate for an OS kernel, for example.