Hacker News new | ask | show | jobs
by sapiogram 2 days ago
What are you talking about, only Rust actually forces you to handle errors. Go functions merely return a tuple with an error along with the result, with a convention that you must checktror a non-nil error before using the result.

Rust bakes this into the type system, a function can truly return a result or an error.

4 comments

Rust is neither the only nor the first language to contain a result type (https://en.wikipedia.org/wiki/Result_type). Definitely a much nicer way to handle errors though.
In practice you use Go with a linter, so this is not a problem in practice. I have never seen a missed error.

The problem is painstakingly and manually having to build the stack trace, and then unwrapping it few layers above if you ever need to check what error it was.

You don't need a linter. Practically Go code will not compile without declaring and using the "err" returning from a function call.

I have written a fresh example at https://files.bayindirh.io/misc/error_example.go. Give it a Go. ;)

OTOH, gopls is a great LSP, though, and it warns you about this immediately.

The problem is that this stops working once you have multiple assignments to err in the same function.
Good point, but I personally never do that? Instead of doing (expensive) calls, I just assign the result to a variable and use that instead. Accessing to memory is pretty cheap in Go, and it's cleaned once it goes out of the scope, so why not?

Considering some expensive Go calls do the same caching underneath, this is the preferred design pattern, I assume.

What if the method only returns an error, like file.Close()?
> with a convention that you must checktror a non-nil error before using the result.

So, you handle the error in the end, or forcefully and intentionally ignore it. Again, if the code goes boom, it's on the developer, not on Go.

> Rust bakes this into the type system, a function can truly return a result or an error.

Error being a variable or baked into the type system doesn't change the practical result. You must handle the error or purposefully ignore it.

> only Rust actually forces you to handle errors.

When you have two programming languages which makes you handle the error, the word only becomes a little invalid.

Semantics doesn't change the result. You have to acknowledge and act on the error either way.

> So, you handle the error in the end, or forcefully and intentionally ignore it. Again, if the code goes boom, it's on the developer, not on Go.

Except Go doesn’t actually require you to handle the error. You can forget to handle the error, or forget to do a nil check. And Go won’t tell you until it crashes and explodes at runtime.

Technically the user’s fault, but good systems protect the users from their own mistakes.

I agree with you that it would have been nice with Result and option types in Go.

But as someone else pointed out, unhandled errors are very rare in practice in Go because every Go project tends to use static code analysis tools that catches this. When people talking about things blowing up during runtime I can’t help but think that they can’t be serious Go users.

On every build I generally run vet, lint (revive), staticcheck, gosec and test. And that’s the “light” build that doesn’t run leak analysis, fuzzing, race testing, benchmark regression and integration tests in addition. The light build is still faster than the Rust compiler. I haven’t compared the heavier build.

When people pretend the absence of features is a huge problem, I tend to think that these are people who either aren’t regular Go users or perhaps they are more interested in debating languages than writing code.

Let’s not pretend this is something it isn’t.

I'm actually not all that fussed about Result or Option types. I don't mind Go's approach to error handling. I think it should just have language/analysis features built it to ensure that you do it 'correctly', so things don't blow up if you do not remember a part of the system ("this value is sometimes nil") or you forget to do something.

The fact that external tools exist that "every Go project tends to use" to fill common a gap in the type system indicates to me that maybe the language itself could be improved.

You rarely get anything for free.

Look at exceptions in Java, for instance. In theory the mechanism is there to guarantee that errors will be handled. In practice there are multiple schools of thought — several of which purposefully circumvent this mechanism by throwing runtime exceptions because they think checked exceptions just leads to a lot of unnecessary work.

It has been too long since I used Java to remember all the arguments for and against. But I do remember trying multiple approaches and realizing that only using checked exceptions turned out to be a bit too inflexible. That there were legitimate reasons to use runtime exceptions.

Go’s type system is probably “good enough”. Sure, I’d love to have option types and results. And it would have been nice to move some of the things in vet, lint, staticcheck into the compiler. But in _practice_ separating them is actually beneficial. Because it allows you to trade build speed for security. For instance when you are making a tiny change and you just want to make sure it still builds. That’s a _practical_ tradeoff you could not have made if the compiler were always strict.

One thing I wish Go would have adopted is Javadoc-like comments with explicit markup to document parameters, return values and possibly panics. Those were _more_ useful in practice than exceptions in Java because you could force people to be more deliberate when designing “contracts”. When I did Java we used to have the build fail if classes and methods did not document all params, return values, exceptions etc.

I also taught people to write unit tests while _only_ looking at the Javadoc for what they were testing and notating at the code. Even after years of doing this, I’d regularly find that implementations didn’t behave as documented when writing tests. This made people care about interfaces/seams/APIs/contracts and spend less time obsessing over having the compiler save them from sloppiness. It put the developer in the driving seat rather than have them run behind the compiler and just nudging things into a state where it compiles. It helped people think more about why something failed from a design point of view rather than put all their faith in the tooling.

I think that’s the biggest mistake of Go: it doesn’t care about documentation and to the extent it does, it introduces really, really stupid, pointless formal rules that does nothing to help developers.

It's not hard to ignore an error in Rust, IME?

Especially as someone who's read a lot of code written by newcomers to Rust.

Unwrap is an explicit action. Checking an error is usually just forgotten. No unwrap can enter your code without the dev being accutely aware of it.
Your Go code won't compile if you don't check the error, though.
It does if you reuse the error variable, which is very common. Example: https://go.dev/play/p/ofL2fG-OIcC

This can't happen in Rust.

Did you ever use go?

``` package main

import ( "errors" "fmt" )

func getMessage() (string, error) { return "DO NOT INSPECT", errors.New("something went wrong") }

func main() { msg, _ := getMessage() // Ignore the error. fmt.Println(msg) } ```

Compiles normally and prints "DO NOT INSPECT"

Here's an example file: https://files.bayindirh.io/misc/error_example.go

I have commented inside the code, but to recap here:

    - If you don't declare err variable, the code won't compile.
    - If you don't use err variable, the code won't compile again.
So, you need to both declare and use the err variable to be able to compile the code. So you can't forget. Your code will not compile.

The only way to "forget" is to declare err as "_", which I call IDGAF placeholder, and this is a deliberate choice to ignore that variable. So you willingly and knowingly ignore the error variable.

Otherwise Go won't give you Go ahead.

Seriously, try to compile the example I have given. It's fresh, so hold with mittens.

If you have

``` a, err := f() b, err := g() if err != nil {} c:=a+b ```

The language will happily build and run, even though it should prevent to let you shoot in the foot.

Intentionally ignoring errors or explicitly not handling them are logic errors. No programming language shall protect you from them.

In a little blunt form: This machine has no brain, use yours.

We can have the same argument about memory unsafe languages: "it should be the programmer that checks that the memory it's accessed in a safe way and not the machine"... and yet after many years and legions of programmers not being able to stop themselves creating memory leaks, we created memory safe languages and we advocate for their usage.

I hope it's clear that what was proposed above is just a toy problem to show the issue. In complex parts of the code, unfortunately, these errors sometimes happen. That's a fact.

A programming language should actually try to protect you from some logic errors. This is why have we programming languages in the first place. Otherwise why bother with garbage collection, types etc? Should we just use assembly then for maximal flexibilty and putting all the burden on the programmer?

It seems like an innocent mistake to me. I think there's a difference between actively ignoring something (like assigning an error to _), and ignoring something through omission. I think computers should try and prevent mistakes.

Go errors if you try to assign a number to a string, so it's clear there is some intention for the machine to catch when your brain makes a silly mistake.

> if the code goes boom, it's on the developer

This is the same argument C (and Zig!) people have for manual memory management. You can avoid memory problems by being a good developer.

While I have my opinions on manual memory management, I'll not open that file today (well, it may leak).

On the other hand, Go explicitly warns and tries to prevent you from ignoring or not handling possible errors. This is a bit different than a happy C compiler which doesn't warn you about leaking memory.

You pasting this comment link on all the replies clearly shows you don't understand what actually went wrong in this Cloudflare outage. But Rust bad.
.unwrap().

I am skeptical about Go's error handling, but there are cases where it is desirable to return both a result and also an error, like returning what can be returned while warning users about any errors. That can be modeled in Rust and other languages as well, though it is the default for Go.