Go proposal: Leave “if err != nil” alone?
github.com
github.com
Oh no, don't even put them in the same sentence. I am no fan of Go, but its error handling approach is beautiful compared to PHP. Every time I call an internal function I have to go through the docs — does it return a NULL, an int, a boolean, or something else? Does 0 signal an error condition, or is it a valid value? Do I have to perform a strict check for NULL/false then? Or is it -1 (see link below)? The situation is generally better with third-party libraries though — they tend to just throw exceptions (if that matches your definition of "better").
You should. It's a perfectly valid exception handling mechanism. It's unfortunate that the name "eval" is overloaded for two separate behaviors:
* catch exceptions thrown as strings or objects
* compile and execute code from a string
Other than the name, they're different behaviors.For your example of openssl_verify see https://linux.die.net/man/3/x509_verify_cert
It can certainly be confusing but at least it is documented.
Also [1]:
>> "I don't know how to stop it, there was never any intent to write a programming language [...] I have absolutely no idea how to write a programming language, I just kept adding the next logical step on the way"
I assume he's better at it 16 years later.
While writing services I often need to centrally log an error and retry the process later on. Exception handling really reduces the mental load regarding error handling in this case.
On a semantic level, there are forseeable errors you need to check locally (for example wrong user input) or exceptional errors which can happen at any time (for example file access errors or out of memory scenarios). Exceptions are a perfect match for the second type.
I don't really understand since when or why exceptions are uncool. It hurts my eyes looking at all that error checking code in other languages.
Also, how about this wonderful syntax:
class OrangeSite
{
Someobject m_object;
public:
OrangeSite(Someobject o) : m_object(o) {
try {
// construct object
} catch (std::exception& e) {
// exceptions for anything in the body above
}
} catch (std::exception& e) {
// exception handler for anything that failed in the initializer lists
}
};
I mean, kill it with fire. That is the only way to catch exceptions that occur in initializer lists and yes you have multiple catches. C# et al haven't reinvented all the same mistakes, but still, there's still two ways to control flow: return values and exceptions.What is really needed is the ability to do the same sort of filtering on error types. You could actually do it in golang with a type assertion, but it would require implementing a type that implements the error interface and type assertions. It's ugly.
Regarding your errors/exceptions, I've heard this before and I agree with it. There are error conditions, from which the program can and is expected to recover, and exception conditions, from which the program cannot recover. Out of memory is a perfect exception condition.
However, this semantic distinction is entirely lost - a lot of error conditions are reported as exceptions. In my above example you have absolutely no choice in the matter; having no error type, languages like Java/C# etc heavily encourage, even if they don't mean to, return by exception.
The golang and rust mapping for these concepts is panic and... panic! respectively. You can catch and recover from panics if need be in golang; in Rust panic handling is bounded by thread and if a panic occurs on the main thread, the runtime triggers an exit. As far as I'm aware, the only way to recover from a panic is like this:
fn unrecoverable_error() -> thread::Result<()> {
return panic::catch_unwind(|| {
panicking_function()
});
}
with panic="unwind" set in cargo.toml. If the thread::Result seems bizarre. it's because panic::catch_unwind basically spins up a thread to isolate that panic.Thus panics in Rust really are for things that should absolutely crash the program there and then. Everything else is a result type and you can shove any type you like into the error field. Golang is a bit more forgiving in that panics can be caught, but the language still encourages the use of errors rather than panics.
At the end of the day, at some point there's going to need to be a translation between some of the internal errors and what the end user sees and this process is going to end up in some boilerplate, be it lots of classes implementing some exception superclass, type or interface, or a bunch of if statements to handle exceptions. I don't think this is avoidable and one line magic code makes me uneasy - I have some experience now managing developers who like magic and want to avoid, at all, costs, translating errors to have friendly user output.
Lastly, there are workaround to this sort of boilerplate. Here's a simplified snippet I use to write this stuff whenever I write go:
snippet errif "Call function then check error" w
${1:var}, err ${2::}= ${3:func}(${4:args})
if err != nil {
fmt.Println("$5 %v", err)
return err
}
$0
endsnippet
It's 2019, so, most editors support this kind of thing. I'm using vim, but VS Code supports it too if you feel the need for a whole browser rendering engine just to edit some text ;) the placeholders allow me to fill in the template. Every time I find myself writing boilerpolate code, I write one of these and automate the task as much as possible. vim-go has a bunch of them: https://github.com/fatih/vim-go/blob/master/gosnippets/UltiS... and I'm not sure how I've programmed all these years without this kind of thing. I have snippets for everything.Working with different kinds of people, beginners and seniors, I learned that everyone has their own way of thinking about errors. It's hard or even impossible to get a consistent code style. Code formatters like go-fmt surely help. But do you trust everone to handle each error case appropriately?
What I like about exceptions is, that our software handles each and every possible error now and in the future consistently in a fail-fast way with an accurate stacktrace for free. I can sleep well, because I can be sure our main loop won't crash. Everyone can write plugins without me moving a finger. It frees us to think about other things first and errors second.
Sadly, the Go team apparently decided all of it was just too involved, so they picked the minimalist try().
[1] https://gist.github.com/networkimprov/961c9caa2631ad3b95413f...
That said, exception handling syntax is ugly and cumbersome in most languages I've seen. Whether it's try/catch/finally with braces or begin/rescue/ensure/end or whatever. It's also rarely written in a way that tells the reader where exactly the exceptions come from, it's just a blanket for a large block of code.
Something that ties the handling directly to the statement that breaks, without the noise of including the 'exceptional' behavior alongside the main logic, might be an improvement:
result = do_something(x, y, z)
handle SomeArgumentError with my_nre_handler(x, y, z)
handle RecordNotFound with missing_record_handler(x)Isn't this is exactly what happens in Go code with the current implementation? At least all the Go code I've read seems to do this.
If you don't know what to do or can't do anything, you return the same error, preferably adding more details to the error string. If you do know what to do you handle it.
I much prefer that everyone has to make an explicit choice on what to do; I find following code with exceptions much more painful.
The community convention seems to be keep rewriting the `if err` dance, but there's nothing encouraging that in the language itself.
https://play.golang.org/p/3ujNLwOdXZf
But explicitly having to handle them versus implicit exception based handling is a tradeoff. Often it isn't clear which exceptions a function might throw unless you've read all the documentation or you're the one who wrote it. When I'm coding my own stuff I'd tend to prefer exceptions but in larger teams I'd tend to prefer explicit handling.
And then there are languages, e.g. Haskell and its Maybe, that manage to combine the best of both worlds.
And unless you catch the common ancestor for all exceptions, it’s difficult to know what you need to look out for. This is especially true in dynamic languages like Ruby.
To my mind, exceptions enter a language when the language lacks the semantics to expose them in the return value, where true/false/null/errno is too primitive for larger systems.
Option, Result, and Either are great alternatives that allow you to propagate problems to deal with them at a higher level, with explicit acknowledgment that an error might occur.
Exceptions can be ignored and caught in random places with little to no context, so long as they’re further up the stack. Or you just get stuff like:
try {
Application->runTheWholeThing();
} catch (e) {
print “something went wrong”;
}If you are catching error in random places, you are doing it wrong. As you pointed out, context is critical. My point is generally, the location of code that generated the error may not have sufficient context. If you have enough context locally (or through further inspection) on how to proceed, then you handle it at that level. If you don't have enough context, then you send it up the call chain.
In your code example above, if after that catch, the program exits, then I argue it is correct. Some exception that was unrecoverable happened somewhere in the application, thus it needs to halt. If the error is recoverable and the application should continue, the exception should be handled somewhere else in the application.
But as a user I hate them and the reason a simple: Because the developer doesn't have to handle every case, many developers just don't know what types of errors/exceptions can happen at runtime and unless they catch all Exceptions at the top-level, their programs will crash every once in a while.
Yes, it is a bit unfair to judge a feature like exceptions by the resulting code quality, as you certainly can use them responsibly, but my personal impression is that exceptions mislead developers to writes code of lower quality (myself included). That is why I like the Go approach to (some extent).
Easy to deploy, easy to write, tons of docs on the official site, massive deployment bases, and also genuinely holding back progress in reliable software writing; both cover up the irreducible complexity in professional software development that other, fussier tools, force you to handle.
There has to be a better way.
not the end of the world, just sad. i do wish there were a type-unified zero value, or that "" and 0 were the same as nil, or some other solution here
I get that Go is targeting a systems niche where error handling is more sensitive. I think there will be fewer complaints about Go's error handling when people get a better sense for when to use it.
Yeah, that is a problem. At least they had to manually type it out and (maybe) think about how to handle the error during that time. To streamline this is to further exacerbate the issue. They will do a try/catch try/defer `log.Println`.
With Python try/catch allows for ultimate laziness. It allows for 1,000 line try/catch blocks that can hide/capture errors that bubble up from anywhere. Of course this is bad development but it's possible. Currently with Go it is impossible to forget about error handling. You have to consciously make a choice on what to do with it. Even if that choice is to log/print it out.
Usually the answer is to wait for an actual exception and if it's not something you could actually anticipate, then write a tiny try block that just handles the exception. The whole point being that I want it to fail if there is an unknown exception. Which in Go, you wont fail at runtime if you are just printing errors as a habit( I think, I have not done much with go).
That said, a lot of times it makes sense to return errors, and perhaps there in lies the difference between an exception and an error.
You’re describing some other language that forces you to deal with errors, like Haskell or Rust.
A bare
io.WriteString(w, “ok”)
will happily compile despite returning a completely unhandled error. a, b := someFunc()
fmt.Println(b)
throws a compiler error like it's the end of the world if you define a variable that isn't used.It's not a big deal.
A quick work around for that example would be to a line: `b = b`.
Go vet complains but it builds and runs.
There are actually better ways. Rust and Haskell not only fix this problem without breaking function composition... they make it so that the compiler is able to catch missing error handling logic.
The golang way of doing this is not the ideal way, but it does fix your issue with python.
Not true at all. The easiest example is:
func main() {
fmt.Println("")
}
The error is not being handled. An exception-based language doesn't have this issue, neither does a language that actually forces you to handle errors. do_something if err
I've read the other proposals, and in fact one of them (catch) is similar to what I do when I'm coding just for myself: value, err := os.Open(filename)
exit_on_error(err)
where the code for exit_on_error is simply something like: if err != nil {
fmt.Println("Something went wrong!")
os.Exit(1)
}
you could make it a bit more robust: value, err := os.Open(filename)
handle_error(err, ERROR_FILE_OPEN)
where ERROR_FILE_OPEN could be a simple constant, and in the method we have a switch with constants and messages. not a zero cost abstraction, but if you have to handle an error, it probably wasn't a cheap operation either, so the extra call won't be an issue if (error) return error;
It's the same style as go, but so much more compact. If `go fmt` were to render the error boilerplate as if err { return err }
…instead of the three line boilerplate it insists on, then it would go a long way.That said, I still support `try()`. It looks good to me, especially coming from Rust.
But "Go users like it" does imply "therefore it's not too badly mismatched to what go users are trying to do with it". Likewise, "Non-go users don't like it" implies "therefore it probably isn't a perfect fit for what non-go users are trying to do".
It's still a big issue overall.
From my experience with Go I find that only these lines fade into the background: something, err := someFunc() if err != nil { return nil, err }
I find that whenever any actual error handling logic beyond forwarding tends to really jump out at me in code reviews and such.
I think that the try() stuff removing the humdrum repetition is a good idea precisely because it makes all explicit error handling stand out like a beacon. I might name it "pass" or something but that's not as important. If I recall correctly, it vaguely resembles a solution you recommended to me on at least one occasion.
Keeping things as they are seems to me like the worst possible option.
I agree that "try" is better than the status quo, not because the if statements are lulling, but because they have the effect of triple-spacing Go code and making it harder to read.
My intuition is it will alleviate that but I’m not sure.
To be honest I'm not sure what you're suggesting at all. The reason the default error handling strategy for so man languages is to defer upwards is because that's by far the most convenient behavior for development, and is not the worst decision in prod.
Folks don't seem to find it confusing. Their programs crash with a stack trace. Certainly exception propagation has its own issues, but the default strategy isn't a thing I'd call out as bad. Indeed, if anyone can't produce a good crash-to-stack (cough, Haskell, cough) they get called "unfriendly" for developers.[1]
Meanwhile, I'm hardly the only voice in the choir of folks saying that Golang's error handling is frustratingly repetitive and can sometimes introduce surprising issue due to variable reuse, capture and shadowing.
> I agree that "try" is better than the status quo, not because the if statements are lulling, but because they have the effect of triple-spacing Go code and making it harder to read.
Both of these can be true without invalidating each other.
[1]: This is not to say we can't do better. Common Lisp is quite rightly famous for its Conditions and Restarts which are (imo, of course) pretty much the best error handling system ever created, allowing developers to embed recovery strategies AND rich exceptions in their code with syntactically and semantically obvious constructs and then pass the calling code the responsibility of choosing a mitigation strategy (or crashing).
By default, an ignored return code does nothing. The situation is shuffled under the rug.
By default, an ignored exception is loud.
Error codes have to be handled to become louder.
Exceptions have to be handled to become quieter.
The control flows underlying exceptions are syntactically invisible except at the source and destination sites. However, they reliably occur. They reliably occur because they are invisible. If exceptions had to be propagated by some visible assistance through all the layers they cross, they would become unreliable.
The problem/trade-off with exceptions (the bad aspect of how they are invisible in their own way) is that if we have statements like S1; S2; S3, we can never be sure that S2 is reached, or if S2 is reached then S3. S1 can call some function that throws, and so can S2 or S3.
Exceptions can cause partially executed statement flows to be unexpectedly abandoned. And that means that whatever those statements were doing remains half done because the programmer assumed that if S1 is reached, then S2 and S3 are an iron-clad given.
Programmers using exception-based languages have to train themselves to use unwind protection mechanisms in all situations fitting the pattern that "if we have executed S1, then S2 must execute to complete/undo/clean-up whatever S1 started". They must never rely on ordinary sequencing, except when it is obvious that the code doesn't throw (for whatever reason: triviality, or all being in the same module put together by that programmer or whatever). If any external code is involved (statements inserted by a macro, foreign callbacks, calls into third party stuff), all bets are off.
Obviously, Go does some things to mitigate the risk of ignored error returns; the damnable use requirement means that multi-valued functions returning errors either have to explicitly ignore the error by assigning it to "_", which feels as squicky as it should, or be assigned to a variable with which (usually) something must later be done.
It's not perfect, I'll freely concede.
It makes me think Go's comprehensive error handling can be compared to the performance of C. If you need it, it's great; if you don't need it, the effort you invest in it will detract from something you do need.
Just compare how good the error handling is in OCaml: http://keleshev.com/composable-error-handling-in-ocaml
They are strings. They are implementation of interface with a single `to_string` method, after you got `error`, you can't say (you could use switch on type, but you should guess which type could it be), so basically you have an opaque object with a single `to_string` method, which is equivalent to string.
I can't fancy a more error prone way than that which Go choose. How you match the errors, you match strings? You match types? You don't even know what types could there be, since all what the type system says is that function returns error.
The beauty of exceptions, on the other hand, is that the default option is the safe one. Sure, forgetting to add error handling may leave you presenting a user with a stack trace, but at least you're not billing them for something that never gets delivered.
The problem with exception error handling is that it's all too easy to get it wrong (by forgetting to complete the handle code) and when it does go wrong, it goes wrong silently, introducing a risk of leaving you with corrupt data. In production.
In either case, error handling needs to be code reviewed. The best thing to do, is to make the right thing the easiest, minimal friction thing to do. Unfortunately, getting off the "happy path" is often a messy business. My suggestion is to explicitly implement a standard "developer scaffold" to be used when filling in the error handling during development, with penalties for not using it. This makes it easier to find where error handling needs to be fully fleshed out.
The beauty of exceptions, on the other hand, is that the default option is the safe one. Sure, forgetting to add error handling may leave you presenting a user with a stack trace, but at least you're not billing them for something that never gets delivered.
The entire reason why Unit Testing and Test First made it into Extreme Programming and Agile methods, is that it was way too easy for end-users to see error notifiers and stack traces in production in Smalltalk.
It's much more difficult to "forget completing the handling code" than it is to overwrite a golang error value and not handle it (which does happen in code bases). The default behavior of unhandled exceptions is to bubble up, unlike golang errors that can get silently dropped quite easily, and I've seen this is in large code bases. Unless you're writing
try { ... } catch (..) { /* do nothing */ }
in which case you explicitly opt into doing nothing, and for which there are linters that catch this sort of behavior automatically.On the other hand, even the simple
func main() {
fmt.Println("")
}
doesn't handle errors.There are linters for golang. No reason why those sorts of tools and community norms shouldn't squash those sorts of behaviors. (As has happened for race conditions in golang!)
a, err := foo()
b, err := bar()
c, err := baz()
err doesn't get handled in the first two cases, and the linter doesn't complain.And race conditions in golang are very much possible, and cause issues in prod.
Of course. They're just a tool.
For the amount of concurrency done, golang is doing pretty good. I know of no other programming community which has such community standards on using race condition checking to the same extent as linting.
Which is why it is strictly superior to use a language feature which does not have this issue to begin with (e.g. exceptions or actual compiler enforced error handling like Rust).
> I know of no other programming community which has such community standards on using race condition checking
I assume you're referring to the golang race detector. It's better than nothing, but then again, it's a tool and is not guaranteed to find all issues that may arise. Compare (again) with Rust, or with languages with proper immutable data structures like Scala and Java (e.g. https://immutables.github.io), not to mention https://openjdk.java.net/jeps/8208520 or https://clang.llvm.org/docs/ThreadSanitizer.html
Or Java?
Compare (again) with Rust, or with languages with proper immutable data structures like Scala and Java
To paraphrase you from earlier: As far as I know, race conditions happen and leak out into production with Scala and Java.
The lesson of golang, and really the lesson of programming in the large for the past 30 years, is that it's not enough to have mathematical or methodological power in guaranteeing correctness in a program. If that were the case, formal methods would have won decades ago, and we'd all be using such environments. It's not about having the most rigorous widget. It's getting programmers to do the right thing, month after month, year after year, in large numbers, across different management teams. Arguing the strength of constructs within programming languages is just an idle meta-anatomic measurement exercise.
That said, I like lots of things about Java, Scala, and Rust. I just happen to really like golang for a different set of reasons. Golang is concurrency+programming-in-the-large-human-factors "Blub," in a way that refutes PG's essay about Blub.
Yes - that was implied in the same sentence where I wrote "(e.g. exceptions or actual compiler enforced error handling like Rust)." :-)
> As far as I know, race conditions happen and leak out into production with Scala and Java.
They do. But there are more techniques in those languages (such as immutable collections, as their type systems are actually able to model and implement them) to mitigate the issue.
> and really the lesson of programming in the large for the past 30 years, is that it's not enough to have mathematical or methodological power in guaranteeing correctness in a program.
I agree there. I actually don't abide by the ideas that using a language like Haskell or Idris or what have you will automagically grant you superior or error-free software. I actually like what talks like these have to say:
* https://www.youtube.com/watch?v=dWdy_AngDp0
* https://www.youtube.com/watch?v=449j7oKQVkc
However, that does not mean we disregard established practices, only to try to reinvent them in a bad way as we see golang trying to do (no generics - even though many other languages implemented them "correctly", bad error handling, no sum types, null pointers, and so on). They purposely disregarded these works without good arguments at all, and now they're struggling to find workarounds, or continue to dismiss them as non-issues, even though they clearly are.
The way I see it, the moment Java gets fibers and value types implemented, and GraalVM's native compilation is mature enough for use, golang's appeal as a devops langauge where it ended up (however low it is currently - as it's mainly over hyped) becomes even less. C#/.NET is the other alternative making very good progress, and they already have async, value types, and NGEN (though I don't know how mature the latter is for production).
> programming-in-the-large-human-factors
This seems to continually get mentioned without anything to back it up. Sorry, but from what I've seen, Java and C# are strictly superior for "programming in the large". Things like "fast compile times" aren't even a factor in front of Java and C# with their incremental compilation. And the fact that the language is "simple" just means that you will end up with more verbose and complex code, with more places for things to go wrong, and yes I've seen it in larger golang code bases.
Except, when it's not. Exceptions tend not to solve the problem, only make it subtly worse. The biggest wart on exceptions is the fact it introduces non-local control flow. All of the sudden any function you call can cause you jump out of your current function. In any situation where an unhandled error will corrupt your state, exceptions have that problem as well, on top of the fact that they are invisible.
An `err` aliased to `_` or shadowed can be found by a linter or a human reading the code. A function `foo()` causing your stack to blow up and possible corrupt any IO you are doing is worse. Therefore, the guys in Java-land discovered CheckedExceptions which were even more controversial, and arguably led to languages like Go and Rust dropping exceptions in general.
Such things probably wouldn't work in systems languages like Go. I'm surprised we haven't seen more push for functional system/embedded languages.
So I don't see this proposal accomplishing anything beyond generating heat on commenter keyboards.
f, err := os.Open(filename) or return 0, err
Actually, I'll just open a proposal: https://github.com/golang/go/issues/32848There are many ways error handling could be changed that would make the system more complex (such as the "expect" or "catch" proposals he links to). 'try' is to handle the super simple, repetitive case.
One more downside I could see is that people will just start using 'try' without thinking about how they should really handle an error, but I feel like that already happens today, so 'try' should just simplify code.
But instead of finding a general way how we can write functions that can do this, we want to add a function that looks like all the other functions but doesn't work like them.
So even if I like what we are trying to achieve here, it feels wrong to me.
I like how Rust does it: all macros and special built-ins like these have names ending with an exclamation point. So when you see for instance "try!(something())", you know that this "try!()" is not a normal function call.
The complaint about go programmers silently ignoring errors seems off to me. If you have a function declared like "func foo() (int, error)" and then try to silently ignore the error like "x := foo()", the program won't compile. You have to go in and say "x, _ := foo()" which does look pretty weird. If you do that, you've ignored the error. It's no different than "try { foo() } catch(Exception e) { }" which I guarantee you plenty of programmers do. The resulting faults are their problem; someone tried to help them but they shot themselves in the foot.
The real problem is the special case of functions where you want to ignore the value. You can then ignore the error freely. (A good example is fmt.Printf. It returns (n, err). Do you ever look at either? Did you even know that it returns something?)
If anything, that's the issue I'd want to fix. Calling fmt.Printf in a "void context" should say "assignment mismatch: 0 variables but Printf returns 2 values". Then the compiler completely prevents you from ignoring errors (and useful results) without making a conscious effort to do so.
As for errors themselves, generally I quite like the detail produced by a chain of errors.Wrap. It is more concise and customizable than a full stack trace, but still lets you track down problems. The annoying part is that you can't translate these errors into a less-detailed error for external consumers easily. For example, you might have a chain like "get foo: authenticate: lookup session in database: context deadline exceeded". You want your gRPC service to then return codes.DeadlineExceeded so the client can retry. But you don't REALLY have a good way to convey that programmatically, resulting in ugly code like `if strings.HasSuffix(err.Error(), "deadline exceeded") { return nil, status.Error(codes.DeadlineExceeded, ...) }` which I think is probably prone to error and annoying to write.
Checking for errors, though... it doesn't annoy me at all. In 46k lines of code I've written in the last year, "err != nil" is on 1500 of them (500 in tests), and I am super super paranoid about every possible error. It just isn't that big of a deal. "if ...; err != nil" is something you'll probably type if you write go. I don't think it's a plus or a negative. It gets the job done, it's easy to understand, and it empowers the programmer to do the right thing when the program fails. The rest is up to you.
I don't think I've seen that suggested anywhere, which is surprising to me. I guess it's fear of scope creep, of allowing syntactic shortcuts that might be ambiguous or dangerous in other circumstances? But I would have thought it could be restricted to error types.
Then only types which implement that are in that Interface. There's your enum/sum type. Not ironclad, but safe enough to get things done.
No other solution allows the compiler to catch a runtime error at compile time.
It's mindblowing, but you can watch the reaction of most people, there is almost never an step of "let's find what the problem actually was and make sure it doesn't repeat later".
Proposal: leave "if err != nil" alone? (Go)