Because I can't stand Golang error handling. It's repetitive, it's error prone, and other language features interact with it so that when you make a mistake it can be as hard as a double free to track down where the erroneous default value was introduced.
On the other hand, using Rust, Ocaml, F# or Haskell I understand how my code composed and I can be confident I can just use an error handling strategy. The only complexities appear, as with everyone else, when we have asynchronous code.
So I don't mean to disagree with your feelings, but they're sure not mine and they're part of why I don't use Golang. I was excited about try() because it at least addressed the most tedious part.
Concise languages can be more challenging to read because you have to understand more about each symbol/word in the language. But verbose languages can be more challenging to comprehend because there's a lot of symbols which don't signify anything.
I suppose concise / implicit is fine when the thing that's being hidden can't go wrong, like in:
[i * 2 for i in 1...10]
The loop counter increment logic can't possibly go wrong, so it's fine to not think about it.
Regarding error-handling, don't you want to think about? If you're calling a() followed by b(), what should you do if a() fails? In some cases, b() shouldn't be called, but in others, it should, like deleting a temp file. And if you have to think about it, it's better to be explicit?
To put it more simply: yes, the developer should have to think about what they do in the case of an error. And then the amount of work they do -- and the code produced, and thus the work reviewers have to do -- should be proportionate to how unusual the necessary error handling is. When I see explicit error handling, that signals to me "hey, this is an important case that we need to handle in a particular way".
Great comment. I would add how unusual _and critical_.
One of the things I love about Python is that while I know errors can occur on practically every statement written, I only have to add error handling for likely / expected / critical errors. Any unlikely errors that occur, even in production, will show a detailed stack trace (lots of context), making them easy to fix.
In my experience, things work as expected 98% of the time. For some software, like a pacemaker, checking the execution of every single line of code and even having redundant error checking is not overkill. For other software, like the backup software I work on, having one customer out of 1000 get a weird error is something I'd rather deal with as a support ticket rather than having to anticipate it while writing code.
Of course error handling is important, but requiring 3 lines of error handling for every 1 line of actual code has kept me from investigating Go to replace Python for HashBackup. I'd love to get the performance increase, but not for a 4x expansion of LOC.
Or try Rust. ^.^
In this case, I don't think anyone is arguing that error handling should be implicit. They're saying there should be an explicit way of saying "handle this error in the common way." This actually makes the distinction between common and uncommon cases more explicit, because their differences aren't buried in boilerplate.
It's similar in Go since you can't compile with unused variable so you need to explicitly discard the error with _. Ex: result, _ := func() This is for multi-value returns, for single value you can even omit the _
Your example re Go errors is incorrect. The go compiler allows you to ignore errors in returns without any compiler error.
For example
err := doThingThatErrs()
and
doThingThatErrs()
are both valid Go code.
Try Return [Bla(), null] Catch err Return [null, err] End
I do like this syntax better, bc the different scopes cause a lot of nesting
But yeah, Go dev do not see VB's features as being "features."
VB's biggest problem IMO is that it tried to compete with C# instead of maintaining its original raison d'être which was to be a highly productive tool that required very little programming skill to be able to start building real solutions for business use.
But I digress...
Go's error handling isn't any more error prone that writing if statements for all the rest of your code. if err != nil is no different than if age < 18. Either one is a branch in the code. I can literally count on one hand the number of times in 6 years of full time go development that I've seen people miss writing the if err != nil.
Being explicit is good. Spreading out the logic is good. Cramming a lot of logic into one line is bad.... and that's the sole purpose of try.
Maybe there's another way to make error handling better in Go. I'm not averse to looking into that. But try wasn't it.
You're talking about writing if err != nil being tedious, but what about matching Results from Rust, isn't that tedious? What about writing proper catch blocks in java or C++ or python, isn't that tedious? It's all just logic.
I absolutely agree. Beyond the human parsing aspect it also makes commit changes easier to reason about and review. I want functionality to be limited per-line and view the ability to combine a lot of functionality into one line as a liability more than a benefit.
Go's error handling isn't carefree or hands-off, but that's because error handling is serious. Especially in network code and cryptography.
That said, I am not a huge fan of fluent interfaces. I much prefer passing in a struct as an "args" parameter, in most case (but not all.)
If you only need to propagate the error or cleanup resources then propagate the error, then all you would write is... Nothing. And cleanup+propagation is by far the most common error handling strategy. In Java and Python exceptions even add context for you automatically to help track down what happened.
> you only need to write [error checking] when you actually have something meaningful to do.
Although it's the subject of a lot of ridicule, `if err != nil { return err }` is actually bad Go code, and not often written by good Go programmers. Errors in Go are, at a minimum, annotated with contextual information before being returned. Frequently, they are programmed-with in other, more sophisticated ways, depending on the domain of the program.
Shifting your mindset to understand errors as something significantly more important than the off-gassing of your program's execution is worthwhile in general, and, in Go, fundamental.
What surprised me when I last wrote Go was that there was no out-of-the-box solution to adding a stack trace to the error.
This is all to say: I can understand why they aren't more naturally part of errors, and I think it also helps explain why they are part of panics.
And with all that said, I wouldn't object to making stack traces easier to add to errors, as long as it was opt-in.
Like the people who wrote the Go stdlib? Because that's filled with those - just look at the net/* packages.
Many programs can benefit from a more structured approach to error management. But once you get past the minimum (above) there's no one-size solution for what "a more structured approach" looks like. I really enjoy how upspin.io does their errors package, though it is somewhat esoteric. I'm also reading and generally liking how Cockroach does things, though I don't like the coupling to Protobufs.
So using these functions is becoming part of what good Go programmers do.
I would much rather have an error with too much or duplicate annotation than one with not enough. And I would further argue that, yes, in the stdlib, errors are generally under-annotated.
err = doThing()
if err! = nil {
return errors.New("Error doing thing", err)
}
This doesn't add any useful information for whoever is reading the code, it's just boilerplate that you learn to skip while reviewing, while hopefully not missing any important thing that does happen on the error path.As for 'programming with the error', I would like to see an actual use case for constantly doing this, and why exceptions would prevent that pattern. The only one I can remember is something highlighted as a 'good practice' by Rob himself: write everything you want to a bufio.Writer, without checking the error messages, and then calling Flush and only then checking if maybe something failed. If this is good, safe, sad-path-first, errors-are-values style... then my taste in programming is obviously bad. Obviously, the same could be achieved with exceptions.
If above is true, then how about:
err := renderTemplate()
if err! = nil {
return fmt.Errorf("Error rendering template: %s", err)
}
The end error could then be something for example: Error rendering template: Compiler has failed: Cannot load template: File /tmp/test.tpl was not found
------------------------ ------------------- -------------------- --------------------------------
| | | |
Returned by that | | |
example Returned by the | |
fictional compiler Returned by the |
fictional template Returned by the fictional file
Loader reader
I didn't even twist your example, and yet you can already see more information. And with that information, even a user can understand what's going on clearly. So ... more useful?Regarding you example: the code itself still contains redundant information for someone reading it. True, the error ends up being nicer, though I would argue that the user would have been better served with a simple 'failed to load template file: /tmp/test.tpl', no need to show the pseudo call stack (so, only the fictional template loader should have been wrapping the error,for this particular case). And for a developer, the full call stack may be more useful. Exceptions would give you both for free - a nice message that can be shared to the user by whoever caused the most understandable error, and a call stack that can be logged at the upper layer so developers can see it if a bug is logged, and get a much fuller context.
But then I am super zealous about making sure my error messages understandable without the need to track down other information. For example, I want to know that (something like) "the config file needs group read permissions" not "file cannot be opened." But maybe others value ease of programming more than I do and are less concerned about error UX than I am?
Using Rust, which makes errors explicit like Go, has been eye-opening to me. My programs never crash because I've handled every error condition. No effort on my part. No tests needed.
By the way, Go is much happier to crash than Java - for example, a simple array index out of range will cause a program crash in a typical Go program, where it would only cause a request failure in a typical Java program. Not sure how Rust handles this.
Finally, choose that isn't tested (manually or automatically) is very unlikely to work. Maybe you can guarantee it doesn't crash, which is a much weaker guarantee, but I doubt even fully proven code (like seL4) is all bug-free before ever being run.
Rust handles your out of range scenario the same way Go does.
If any of this matters to you, the good news is that Kotlin's sealed classes (and soon, Java's sealed classes) allow you to easily implement your own Result-like sum type.
I understand how Result forces you to handle the possibility of an error when you want to access the actual return value, but I don't know what happens if you aren't planning on accessing the return.
I'm also curious how Result-based error handling composes. For example, if I want to sort a list of structs where my comparison function may fail, can I use the built-in sort function, and still get any error that may have occurred back?
With (unchecked) exceptions, this is trivially easy - the sort() function doesn't need to be aware of exceptions in order for them to be propagated through it. With checked exceptions in Java, you need to go through a little dance of wrapping and unwrapping, but it can still be done. If I understand correctly, in Haskell this can be done with the Either monad and liftM, though I can't claim to understand the specifics.
Is there a Rust solution?
Yes, the default semantics is that it will stop when the first comparison fails and give you that error. You can write slightly different codebase if you want a list of all errors instead.
- It's hard to spot outliers. This leads to occasional bugs that tend to get easily overlooked in code review. Also, it makes code reading harder when an "if err" block is subtly different. The most common case here being "if err == nil" (sometimes bug, sometimes on purpose) - super hard to notice.
- You say you have seen missed "if err" blocks only a few times. I say that's 100% too many; every one of them in my experience was a subtle bug (possibly comparable to off-by-one errors in C).
- When I need to focus on understanding/analyzing the optimistic path in a fragment of code (always the first thing I do when reading), the everpresent "if err" blocks introduce tiresome visual noise and make the reading/grokking process slower and harder (having to constantly try and mentally filter out some 80% of what my eyes see).
Also, what editor/IDE do you use? The reason I ask is because of this: https://youtrack.jetbrains.com/issue/GO-7747
if err != nil { return ...,...,err }
If there were a shortcut for returning that error + zero values without interfering with the function call which produces the error (as try does), I'd prefer it. Something more like check(err). We'll see what they come up with next though to try to address this.
I can't say I've ever had problems tracking down an error, not sure what you mean about default values - surely if you check the error you won't use the values returned. My only problem with go error handling is the verbosity, which isn't a huge deal.
You didn't have to because there is an precise, robust non-checked exception handling system which takes care of that: the hardware catches the situation, dispatches a handler in the operating system which fixes it and re-starts your program at the original machine instruction to try the memory access again.
Could you give an example?
I think you’re talking about something different than what I’m understanding. One of the major frustrations I have with Go error handling is the lack of stack traces, which means I often have to modify code in order to find out where an error occurred.
I’m pretty sure that’s not what you’re talking about, though.
My impression of go, is that it is very boring to program in it, and some decisions weren't thought well. For example if you use anything else for numbers than int, int64 or float64 you will have very bad time. Lack of generics forces you to duplicate your code, duplicating increases chances of errors and make it harder to fix bugs. The errors are passed as values, but then you need to use different return value to pass them, defeating the whole point of having that. On top of that the language is very rigid.
I'm wondering if introducing macros could solve a lot of those issues.
Same could be said about assembly language.
Don't you just mean you knew where fatal exceptions could be raised? That's substantially different from "fail".