So when I looked at Go for the first time, the error handling was one of the many positive features.
Is there any good reason for wanting try/catch other than being lazy?
So when I looked at Go for the first time, the error handling was one of the many positive features.
Is there any good reason for wanting try/catch other than being lazy?
sounds good on paper, but seeing "if err!=nil" repeated million times in golang codebases does not create positive impression at all
Rust's error handling is clearly better than Go's, but Go's is better than exceptions and the complaints about verbosity are largely complaints about having to actually consider errors.
I'm honestly asking as someone neutral in this, what is the difference? What is the difference between building out a stack trace yourself by handling errors manually, and just using exceptions?
I have not seen anyone provide a practical reason that you get any more information from Golangs error handling than you do from an exception. It seems like exceptions provide the best of both worlds, where you can be as specific or as general as you want, whereas Golang forces you to be specific every time.
I don't see the point of being forced to deal with an "invalid sql" error. I want the route to error out in that case because it shouldn't even make it to prod. Then I fix the SQL and will never have that error in that route again.
With exception style code the overwhelming temptation will be to call `string_to_int()` and forget that it might throw an exception.
Cut to your app crashing when someone types an invalid number.
Now, you can handle errors like this properly with exceptions, and checked exceptions are used sometimes. But generally it's extremely tedious and verbose (even more than in Go!) and people don't bother.
There's also the fact that stack traces are not proper error messages. Ordinary users don't understand them. I don't want to have to debug your code when something goes wrong. People generally disabled them entirely on web services (Go's main target) due to security fears.
Is it? In my experience it's very short, especially considering you can catch multiple errors. Do my users really need a different error message for "invalid sql" vs "sql connection timeout?" They don't need to know any of that.
> There's also the fact that stack traces are not proper error messages
I would say there's not a proper error message to derive from explicitly handling sql errors. Certainly not a different message per error. I would rather capture all of it and say something like "Something went wrong while accessing the database. Contact an admin." Then log the stack trace for devs
Yes! A connection timeout means it might work if they try again later. Invalid SQL means it's not going to fix itself.
But in any case, the error messages are probably the minor part. The bigger issue is about properly handling errors and not just crashing the whole program / endpoint handler when something goes wrong.
> I would say there's not a proper error message to derive from explicitly handling sql errors. Certainly not a different message per error. I would rather capture all of it and say something like "Something went wrong while accessing the database. Contact an admin." Then log the stack trace for devs
Ugh these are the worst errors. Think about the best possible action that the user could take for different failure modes.
"Contact an admin" is pretty much always bottom of the list because it rarely works. More likely options are "try again later", "try different inputs", "clear caches and cookies", "Google a more specific error".
Giving up on making an error message because you only have a stack trace and don't want to show it means users can't pick between those actions.
If you have written a "something went wrong" error I literally hate you.
You're totally misunderstanding what I'm saying. If I have an error the user can act on, I'll make that error message for them. If they can't act on it, I will make a generic catcher and ask them to contact an admin because that's the only thing they can do. It is not my experience that any of these things you've written (try again later, try a different input) are applicable when an error comes up in my apps. It's always an unexpected bug a developer needs to fix, because we've already handled the other error paths. And the bug is not from "not explicitly handling the error."
> Think about the best possible action that the user could take for different failure modes.
What if contacting an admin IS the best possible action? Which is what I'm referring to.
In the case of invalid sql, your route should crash because it's broken. Or catch it and stop it. It's functionally the same thing.
You seem to be under the impression that having exceptions mean people can't handle errors explicitly? It just prevents the plumbing of manually bubbling up the error. It means you can do so MORE granularly. Also, there are some errors that are functionally the same whether you handle them explicitly or not. There are unexpected errors, and even Golang won't save you from that. Golang doesn't even care if you handle an error. It will compile fine. Even PHP will tell you if you haven't handled an exception.
> If you have written a "something went wrong" error I literally hate you.
Lol.
Not at all! It's possible, but it's very tedious, and the lazy "catch it in main" option is so easy that in practice when you look at code that uses exceptions people actually don't handle errors explicitly.
> It means you can do so MORE granularly.
Again, it doesn't just mean that you can; it means that you will. And for proper production software that's not a good thing.
> There are unexpected errors
Only in languages with exceptions. In a language like Rust there are no unexpected errors; you have to handle errors or the compiler will shout at you.
But again, handling an error doesn't necessarily prevent bugs. Just because you handled an error doesn't mean the error won't happen in Prod. It just means when it does, you wrote a message for it or custom behavior. Which could be good, or it might be functionally as effective as returning a stack traces message. It depends on the situation.
For what it's worth, I've never seen people not handle errors that the user could do anything with. If it's relevant to the user, we handle it.
It absolutely does. Checked exceptions sort of half get there too but they are quite rarely used (I think they are used in Android quite well). They were actually removed from C++ because literally nobody used them.
> handling an error doesn't necessarily prevent bugs.
I never made that claim.
> I've never seen people not handle errors that the user could do anything with.
We already talked about "something went wrong" messages. Surely you have seen one of those?
My point is that "something went wrong" messages are for errors the user CANT and SHOULDNT do anything with.
What? Of course there is. Rust added panic! exactly because unexpected errors are quite possible.
Unexpected errors, or exceptions as they are conventionally known, are a condition that arises when the programmer made a mistake. Rust does not have a complete type system. Mistakes that only show up at runtime absolutely can be made.
You cannot force your dependencies to hand you a stack trace with every error. But in languages that use exceptions a stack trace can be provided for "free" -- not free in runtime cost, but certainly free in development cost.
Golang code has a rhythm: you do the thing, you check the error, you do the thing, you check the error. After a while it becomes automatic and easy to read, like any other syntax/formatting. You notice if the error isn't checked.
Yes, at first it's jarring. But to be honest, the jarring thing is because Go code checks the error every time it does something, not because of the actual "if err != nil" syntax.
I've gotten used to Javas getter/setter spam, does that make it a good idea?
Moreover, don't you think that something like Rusts ? operator wouldn't be a perfect solution for handling the MOST common type of error handling, aka not handling it, just returning it up the stack?
val, err := doAThing()
if err != nil {
return nil, err
}
VERSUS val := doAThing()?I'd be very surprised if, in Rust codebases, there's not an implicit bias against wrapping and towards using `?`, just to help keep things "clean"; which has implications not only for debugging, but also for situations where doing something more is required for correctness.
I see two issues with the `?` operator:
1. Most Go code doesn't actually do
return nil, err
but rather return nil, fmt.Errorf("opening file %s as user %s: %w", file, user, err)
that is, the error gets annotated with useful context.What takes less effort to type, `?` or the annotated line above?
This could probably be solved by enforcing that a `?` be followed by an annotation:
val := doAThing()?("opening file %s as user %s: %w", file, user, err)
...but I'm not sure we're gaining much at that point.2. A question mark is a single character and therefore can be easy to miss whereas a three line if statement can't.
Moreover, because in practice Go code has enforced formatting, you can reliably find every return path from a function by visually scanning the beginning of each line for the return statement. A `?` may very well be hiding 70 columns to the right.
1. Most often found in library code, the error types have the metadata embedded in them so they can nicely be bubbled up the stack. That's where you'll find `do_a_thing().map_err(|e| Error::FileOpenError { file, user, e })?`, or perhaps a whole `match` block.
2. In application code, where matching the actual error is not paramount, but getting good messages to an user is; solutions like anyhow are widely used, and allow to trivially add context to a result: `do_a_thing().context("opening file")?`. Or for formatted contexts (sadly too verbose for my taste): `do_a_thing().with_context(|| format!("opening file {file} as user {user}"))?`. This will automatically carry the whole context stack and print it when the error is stringified.
Overall, what I like about this approach is the common case is terse and short and does not hinder readability, and easily gives the option for more details.
As for the second point, what I like about _not_ easily seeing all return paths (which are a /\? away in vim anyways), is that special handling stands out way more when reading the file. When all of the sudden you have a match block on a result, you know it's important.
I'm a huge fan of keeping things simple; my experience has shown me that complex things have lots of obscure failure points, while simple things are generally more robust.
> More terse is not the same as more readable (in fact, I find the reverse).
I generally agree, but I also find that "all explicit" also hinders readability because it tends to drown the nitty-gritty details. As always it's a matter of balance :) And I think that neither go nor rust are great in this matter as one is verbose and the other falls in the "keyword soup" with the chain call, the closure, and the format macro. I'm pretty sure something in between could be found.
Okay, but other than exceptions, whats the alternative?
Of course, this is also scarily non-explicit.
I only briefly tried Rust and was turned off by the poor ergonomics; I don't think (i.e. open to correction) that the Rust way (using '?') is a 1:1 replacement for the use-cases covered by Go error management or exceptions.
Sometimes (like in the code I wrote about 60m ago), you want both the result as well as the error, like "Here's the list of files you recursively searched for, plus the last error that occurred". Depending on the error, the caller may decide to use the returned value (or not).
Other times you want an easy way to ignore the error, because a nil result gets checked anyway two lines down: Even when an error occurs, I don't necessarily want to stop or return immediately. It's annoying to the user to have 30 errors in their input, and only find out about #2 after #1 is fixed, and #3 after #2 is fixed ... and number #30 after #29 is fixed.
Go allows these two very useful use-cases for errors. I agree it's not perfect, but with code-folding on by default, I literally don't even see the `if err != nil` blocks.
Somewhat related: In my current toy language[1], I'm playing around with the idea of "NULL-safety" meaning "Results in a runtime-warning and a no-op", not "Results in a panic" and not "cannot be represented at all in a program"[2].
This lets a function record multiple errors at runtime before returning a stack of errors, rather than stack-tracing, segfaulting or returning on the first error.
[1] Everyone is designing their own best language, right? :-) I've been at this now since 2016 for my current toy language.
[2] I consider this to be pointless: every type needs to indicate lack of a value, because in the real world, the lack of a value is a common, regular and expected occurrence[3]. Using an empty value to indicate the lack of a value is almost certainly going to result in an error down the line.
[3] Which is where there are so many common ways of handling lack of a value: For PODs, it's quite popular to pick a sentinel value, such as `(size_t)-1`, to indicate this. For composite objects, a common practice is for the programmer to check one or two fields within the object to determine if it is a valid object or not. For references NULL/null/nil/etc is used. I don't like any of those options.
It is a 1:1 replacement.
I think you're thinking of the case when you have many results, and you want to deal with that array of results in various ways.
> Result implements FromIterator so that a vector of results (Vec<Result<T, E>>) can be turned into a result with a vector (Result<Vec<T>, E>). Once an Result::Err is found, the iteration will terminate.
This is one such way, but there are others - https://doc.rust-lang.org/rust-by-example/error/iter_result....
This doesn't handle every case out there, but it does handle the majority of them. If you'd like to do something more bespoke, that's an option as well.
This may be a crazy/dumb take, but would it be so wrong to allow code outside the function to take the wheel and do a return? Then you could define common return scenarios and make succinct calls to them. Use `returnif(err)` for the most typical, boilerplate replacement, or more elaborate handlers as needed.
It's the best strategy for short running programs, or scripts if you will. You just write code without thinking about error handling at all. If anything goes wrong at runtime, the program aborts with a stacktrace, which is exactly you want and you get it for free.
For long-running programs you want reliability, which implies the need to think about and explicitly handle each possible error condition, making exceptions a subpar choice.
The main benefits of a Result type are brevity and the inability to accidentally not handle an error.
A funny drawback of the current Go design that a Result type would solve is the need to return zero values of all the declared function return types along with the error: https://github.com/golang/go/issues/21182.
That may be superficially true but don’t forget our brain is structured to optimize every repetitive work or some boilerplates, we can basically use “strcpy” and “string_copy” we are so used to all these that even if repeated a billion times it can be processed fast
In a hot path it’s often beneficial to not have lots of branches for error handling. Exceptions make it cheap on success (yeah, no branches!) and pretty expensive on failure (stack unwinding). It is context specific but I think that can be seen as a good reason to have try catch.
Now of course in practice people throw exceptions all the time. But in a tight, well controlled environment I can see them as being useful.
This is true but the branch isn't taken unless there's an error in Go.
Given that the Go compiler emits the equivalent of `if (__unlikely(err != nil)) {...}` and that any modern CPUs are decently good at branch prediction (especially in a hot path that repeats), I find it hard to believe that the cost would be greater than exceptions.
Because fundamentally the function you called can return different errors at any point so if you just propagate the error the code paths are in fact not spelled out at all because the function one above in the hierarchy has to deal with all the possible errors two calls down which are not transparent at all.
There's no requirement for the calling function to handle each possible type of error of the callee. It can, as long as the callee properly wrapped the error, but it's relatively rare for that to be required. Usually the exact error is not important, just that there was one, so it gets handled generically.
EDIT
Inspired by this comment: https://news.ycombinator.com/item?id=40220147
I forgot about exception stack traces, including "chained" exceptions. These are incredibly powerful when writing enterprise software that commonly has a stack 50+ levels deep.
Error returns are no different, assuming a proper implementation like the Result type in Rust. The difference is, unhandled error returns are found at compile time but unhandled exceptions only show up at runtime, when it's too late.
> Another point this is overlooked in these discussions: Exceptions and error codes can, and do, peacefully co-exist.
Both Go and Rust have panics, which are basically exceptions that are generally not supposed to be caught. They are used for unrecoverable cases like running out of memory or programmer mistakes. There's otherwise no reason to mix the two.
> Another thing about exceptions, especially in enterprise programming, you can add a human readable error message. That is not possible when only returning error codes.
I don't really know what you mean, it's equally possible in both cases. If anything, the error return implementation that Go uses is probably the most optimal out there when it comes to error messages. Most look like:
return nil, fmt.Errorf("opening file %s as user %s: %w", file, user, err)
Whereas most exception code will just dump a stacktrace since that's the default.You generally need to skip all lines that the exception invalidates. That's why it's a block or conditional.
1. err != nil, nondefault return value
2. err != nil, default return value
3. err == nil, nondefault return value
4. err == nil, default return value
when often what you want to express only has two: either you return an error and there's no meaningful output, or there's output and no error. A type system with tuples but no sum types can only express "and", not "or".