Live data from Hacker News

Gopher Wrangling: Effective error handling in Go

stephenn.com

231–240 of 310 posts

Re: Gopher Wrangling: Effective error handling in Go

#231
There was just so much nonsense back in the day around Go's error handling and about how it was so much more straightforward than adding exceptions to the language.

In reality, the only reason why errors in Go work the way they do is that it kept the runtime simpler by offloading checking to the developer. The alternative would've been for Go to support sum types, which would've helped make error handling a lot saner, but that was dismissed because they overlapped a little with structurally-typed interfaces (Go's one really good idea). Oh, and the stupid hack that is 'iota'.

And then Go eventually ended up badly re-inventing most of what exceptions do with errors.Is(), errors.As(), and fmt.Errorf("%w", err).

It's such a hot mess.

Re: Gopher Wrangling: Effective error handling in Go

#232
post #216
post #203

Earlier quoted context omitted.

It is easier to know that lowercase is package-specific, uppercase is exported, than knowing which field is private/public by default. Go reserved keywords: break, default, func, interface, select, case, defer, go, map, struct, chan, else, goto, package, switch, const, fallthrough, if, range, type, continue, for, import, return, var Java reserved keywords: abstract, continue, for, new, switch, assert, default, goto*,…

You have conveniently left out types from go’s list.. with those removed it is hardly longer, and as has been shown (case-sensitive identifiers), not all language complexity lives within keywords.

https://go.dev/play/p/71I57QCycTr This works, so how exactly did I leave them out?

Re: Gopher Wrangling: Effective error handling in Go

#233
post #19
post #11

Earlier quoted context omitted.

One thing I don't get (and would honestly appreciate if was explained to me) is how the Result monad differs significantly from Go's error handling, other than being a "true" monad. Most Rust code I see does things like (from the docs): let greeting_file_result = File::open("hello.txt"); let greeting_file = match greeting_file_result { Ok(file) => file, Err(error) => // handle err }; It isn't much different from: fil…

Result is better because it actually encodes the correct situation. You either get a file or an error. Not neither, not both. Go's encodes instead "you may or may not have a file" and "you may or may not have an error". Not the same thing, and extremely rarely what you want, IME. Other languages also do a better job of helping you verify that you actually handled both cases too. By the way I wouldn't say we need Mona…

I love Rust but honestly the Go way works fine, even if it isn’t as strictly correct as Rust. I don’t think I’ve ever seen a case where a Go function returned neither a value nor an error, or both a value and an error.

What I like better about Rust, and what I think most people are actually complaining about with Go, is that syntactic sugar like the ? operator and functions like unwrap(). It’s a lot more concise and your application logic doesn’t get lost in verbose error checking code.

Re: Gopher Wrangling: Effective error handling in Go

#234

There was just so much nonsense back in the day around Go's error handling and about how it was so much more straightforward than adding exceptions to the language. In reality, the only reason why errors in Go work the way they do is that it kept the runtime simpler by offloading checking to the developer. The alternative would've been for Go to support sum types, which would've helped make error handling a lot saner…

nope! go's error handling is actually good!

it turns out that treating errors the same as normal values makes programs more reliable

lots of people get salty about it, for sure

Re: Gopher Wrangling: Effective error handling in Go

#235
> Make it the top layer’s responsibility and don’t log in any services or lower level code.

> Make sure your logging framework is including stack traces so you can trace the error to its cause.

> For example in a web app you would log the error in the http handler when returning the Internal Server status code.

This is different from how I do it, am I doing anything wrong?

I prefer to make it the bottom layer’s responsibility - so, the first source of the error at the boundary of my application and the library that produces the error, rather than the top level of the http handler.

Go errors infamously don’t include stack traces, so how are you supposed to know where your error originated from if you log it from the top level of the http handler?

Re: Gopher Wrangling: Effective error handling in Go

#236
post #214

Earlier quoted context omitted.

Ignoring an error is a red herring. You have to go out of your way to actually use a special character to do it. No, one real issue that can happen if one is not careful (but fortunately linters help) is variable shadowing which may lead to some errors being unchecked. In general, I find that error handling is not as horrible as some seem to purport.

> You have to go out of your way to actually use a special character to do it. Only if the function returns more than the error. You can happily do this without errors: fh = os.Create("/some/file") defer fh.Close() Needless to say, this is a terrible idea if the underlying filesystem can give you an error at close time, e.g. on NFS. The correct way to write the above code would be: fh = os.Create("/some/file") defer…

Oh you're right. I had forgotten about that.

I think it's mostly an API legacy mistake. Close should probably return (bool, error).

Probably a remnant of coding in C wrt sentinel values.

Re: Gopher Wrangling: Effective error handling in Go

#237
post #116

Go's error handling is a horrible mess: 1. It's easy to ignore returned errors without any compiler warnings. You have to rely on third party tools such as golangci-lint to report missing error handling. 2. Errors don't carry stack traces with them, you have to rely on third party libraries or custom errors to get that functionality and you will only get it for your own code, not in other libraries you are using. 3.…

I agree that there is room for improvement, but I don’t mind Go’s errors that much. Using a linter to make sure errors are checked doesn’t seem like a major problem (you have to run a linter anyway, so what’s the harm?); most Go developers reflexively check errors for everything besides fmt.Println anyway. It would be better to put this in the compiler I suppose, but not a major deal.

Also worth noting that Rust doesn’t require you to use errors either; unused errors are a warning in the compiler unless the return type is a result AND you’re trying to access the valid data. This is a better than Go, but not by much in practice.

The error interface doesn’t bother me too much either. Just use errors.Is/As to determine the type of you’re going to do something special with it. It’s way better than having to create unique error/result types for every function.

Who should add context is definitely a problem, but I’ve settled on “the callee”, but in cases where you’re calling something that doesn’t add context you will need to add it in the immediate caller.

Adding context in Go is significantly easier than in other languages (yes, you can use anyhow in Rust, but it’s not considered good practice to put this in library code), and good context largely obviates the need for stack traces anyway. Context is nicer because it can tell you, for example, which loop iteration you were in when things blew up or what the salient parameter values were—stuff you don’t get from a stack trace. Of course, you have to do a bit of work for this benefit, but fmt.Errorf makes this super easy.

Logging also irks me. You can pass a logger like any other data, but mostly people just use global loggers. I haven’t had the multiple loggers problem, but that’s because library authors in Go idiomatically do not add their own logging. What are the languages that do logging well? I’ve had a horrible time with Python (and I think Java but it’s been 10 years).

Re: Gopher Wrangling: Effective error handling in Go

#238

I differ with the author here, I prefer to log errors as soon as they come into "my" code (e.g. from external library or network call, etc.). This is a good rule for any language, because you always ensure an error is logged once. In Go, you can add additional info from the caller to the Context to log higher level info, e.g. a trace span Id.

an error should be handled in precisely one way

- logged (and control flow continues)

- returned (and control flow returns)

- managed (and control flow (probably) continues)

if you log an error, then you should not return it

if you return an error, then you should not log it

etc.

Re: Gopher Wrangling: Effective error handling in Go

#239
post #55

Earlier quoted context omitted.

with robust and potentially high volume code, the most important feature is good behavior in failure domains. disk full, do you abort or continue once the cron job frees some space; cant alloc memory, do you abort or return a static 503 page? bad contents in some file, do you exit or log the error and carry on? does a bad pyc file generate a good error message or crash python. this robustness is the famed second 90%…

Nothing you wrote is specific to golang's error handling though, and in actuality, ends up being more brittle because it is possible to miss handling such errors. At least an exception would bubble up instead of keeping the program running in an undefined state.

it is my very clear experience that rust programs are more brittle than go programs, precisely because rust makes it possible (even encourages) error "bubbling" via `?`

in practice, go code bases that are subject to even minimal code review have basically no ignored errors

Re: Gopher Wrangling: Effective error handling in Go

#240
post #144

Earlier quoted context omitted.

>3. It's unclear who should add context to error messages is it the caller or callee? Usually it gets skipped, leading to useless error messages Why is that unclear? Let's say you are writting a db client package and a service around it. The package's db.Exec(query) method should return and error that will have an error text received from db if any AND\OR context from the package itself. Then in your service you add…

>>3 > Why is that unclear? The usual advice is to follow what the stdlib does. Let's look at an example. Let's say we close a file and then try to set a deadline on it: f, _ := os.Create("/tmp/filename") f.Close() fmt.Printf("%v", f.SetDeadline(time.Now())) // output: use of closed file Okay, so in this case, it's the caller's responsibility to keep track of the filename and add the context of what file was already c…

I agree, the stdlib is confusing here. A safe rule is just to add the extra context in the caller unless you know the callee adds it. The worst that can happen is you include the file path multiple times and things get noisy.
Post reply on HN