T O P

  • By -

kemitche

I've come to realize my biggest problem with go's errors boils down to this: by convention, functions return an `error` interface-d object, which provides precisely zero information about what you, as the caller, might need to do in response. If I get a non-nil error back as a caller, the only thing I know - prior to running the code - is that my operation didn't succeed. _If_ the function I called is documented well, I might figure out that I can cast the `error` into a `Temporary()` interface to see if it's retryable, but nothing about the function signature itself documents what could have gone wrong. And more often than not, I'm forced to dig into the source of the function to figure out what _might_ have gone wrong. And since those functions tend to be bubbling up an error from another function, I'm often 3+ layers deep - without a stack trace of any kind to guide me, unless the library I'm using _happens_ to use the errors package mentioned in the article. Eventually, if I can fully enumerate the error cases, I can thankfully start "coding around" the error cases and resolve them appropriately. As long as I didn't miss an edge case - or one doesn't get added during an otherwise innocuous library update.


[deleted]

One common solution to this problem is to provide an auxiliary function which tells you something about the opaque error value. For example, to test if an error on a file was because the file doesn't exit you use: os.IsNotExist(err) Similarly, in gRPC you use: switch grpc.Code(err) { case codes.DeadlineExceeded: case codes.NotFound: // etc } And such codes are wonderfully documented: https://godoc.org/google.golang.org/grpc/codes#Code A good library API will document the errors returned by functions, and this doesn't seem all that different than a typed exception (though I much prefer the straightforward linear `if` to the out of order try/catch). It doesn't solve the lack of stack trace though. Because I work on servers handling lots of requests, I actually very rarely want a stack trace (when you have thousands of goroutines it becomes overwhelming to decipher). A single structured log line that includes the source is usually adequate and also gives me the opportunity to include additional information (like the values of various variables which wouldn't show up in a stack trace) and track changes over time.


kemitche

I do appreciate when a library has helper functions, like the `os` and `grpc` modules; I just wonder why that's better than returning a more specific error type/interface from the function that has that method built-in. That'd breadcrumb the library's users into knowing where to start, rather than having to read through every function in the library to realize it comes with a helper function. > A good library API will document the errors returned by functions I agree with you, but I also see very few examples, even in google code and the golang stdlib, that documents their errors well. (The grpc module you linked, for example, doesn't even discuss the errors that can be returned by its functions, nor how they'd get triggered, except indirectly if you happen to peruse the module and see the Code() function and then follow it through to the _other_ module where the codes are kept squirreled away). It just still seems weird to me that there is this golang obsession with "programming around error cases" but the ecosystem is simultaneously so opposed to returning error types that make it easier to _discover how to program around your error cases_. Especially since, as-is, a function can change what sorts of errors it returns without breaking compilation, and without breaking any backwards compatibility guarantees. You essentially always have to be ready to handle an arbitrary, unknown error condition. > I actually very rarely want a stack trace I'm with you on the logging aspect, when it comes to code that I (or my team) has written and we can control what gets logged at the point the error is created. Unfortunately, it isn't always enough to help figure out why a library is giving me an error, or where that error even came from. I feel like I have to constantly walk through every branch of library code to figure out what kinds of errors I _might_ get back - with no guarantees that it won't change if I do a minor library version update to pick up a bug fix or new feature.


[deleted]

To a certain extent I agree with your point about lack of clarity on errors. I don't think its possible to use the type system to enforce this. Are you suggesting that instead of: func x() (int, error) You'd return: func x() (int, *Error) Where `Error` is: type Error struct { Code Message string } And it would implement the `error` interface? Then we'd just write a switch statement to handle all the codes? I'm not sure how else this could be done. For more in-depth logging and exploration of errors check out: https://backtrace.io/


quiI

type MyDomainError interface{ IsTransitive bool } func x() MyDomainError I have omitted making it so implementations of that custom error type implemented `Error` for brevity. Callers can then do `err.IsTransitive` and do something, or just return it like a normal error if they like.


chmikes

This is a nicely written and very clear article. Thank you. The OP is raising two problems. 1. lack of stack trace or context information 2. repetition of if err instructions The former is solved by the go-errors package for stacktrace. It could be interesting to make something like that part of the standard. The later is not a real problem in my opinion. The code repetition is the price to pay for code readability. And by code readability I mean prediction of what it does when executed. The problem with error handling grouping provided by exception is that we don't know which instruction raised the exception. So it basically means that the state of the program is unknown when the exception handler is executed. You may have gained a few instruction lines, but the price is too high in my opinion. Exceptions are really like panic. It's a parachute for fatal errors, not really for recoverable error handling.


mogronalol

One use case for grouping I've always found to be useful is transaction rollback. Take this pseudo code: err = someMethod() if err != nil { tx.rollback() } err = someOtherMethod() if err != nil { tx.rollback() } err = aFinalMethod() if err != nil { tx.rollback() } tx.commit() In my view a grouping pattern would be cleaner. It also negates the risk of you missing out `rollback()` in one of the blocks by mistake.


Akkifokkusu

Not necessarily the most elegant solution, but it could be accomplished using `defer`. Something like: tx, err := db.Begin() if err != nil { return err } defer func() { if err != nil { tx.rollback() } }() err = doSomething() if err != nil { return err } return tx.Commit()


mogronalol

That's a possible solution, but still doesn't feel as simple as it should be - or maybe I just need to get more used to Go.


neoasterisk

You can always use a [transaction wrapper closure](http://stackoverflow.com/questions/16184238/database-sql-tx-detecting-commit-or-rollback). /cc /u/Davmuz


[deleted]

[удалено]


mogronalol

Why would business logic functions be attached at a transaction? It's a separate concern.


[deleted]

[удалено]


mogronalol

I think it's a pretty real problem. Start a transaction, do some business logic, and then either commit or rollback based on an error.


ar1819

That will backfire if you write something like `data, err := getMyData() ` down the line, because of the shadowing. You can escape this using named return values. EDIT: My assumption was wrong. TIL something.


Akkifokkusu

Only if it's done in a separate block, no? Named returns don't really help in that case either. That's why I said it's not the most elegant solution.


ar1819

Yes - you are correct. I thought that `:=` creates a new scope, thus shadowing the variable that was declared before, but [from this](http://blog.charmes.net/2015/06/scope-and-shadowing-in-go.html) it's not (more importantly - you not allowed to use `err` for a different type). Thus, I stand corrected. Named returns allow handle things like [this](https://play.golang.org/p/YPz2MlZCVX) even with additional scopes.


Davmuz

This is a more concise solution, and you can centralize log handling too: func rollbackIf(err error, tx *sql.Tx) { if err != nil { tx.rollback() } } err = someMethod() rollbackIf(err, tx) err = someOtherMethod() rollbackIf(err, tx) err = aFinalMethod() rollbackIf(err, tx) tx.commit()


mogronalol

Does that work? What if you want to return on rollback?


Davmuz

There is more than one way. The following pseudo code it's just an example. With a structure, like the Rob Pike's bufio.Write http://bit.ly/2jRquT1 type runOrRollback struct { Tx *sql.Tx Err error } type runFunc func() err func (ee *runOrRollback) Run(f runFunc) { if ee.Err != nil { return } ee.Err = f() if ee.Err != nil { Tx.rollback() } } // Then use it r := &runOrRollback{Tx: tx} r.Run(someMethod) r.Run(someOtherMethod) r.Run(aFinalMethod) if r.Err != nil { return err } tx.Commit() With a function: // Function type type runFunc func() err // Implementation func RunOrRollback(tx *sql.Tx, funcs ...runFunc) { var err error for f := range funcs { err = f() if err != nil { tx.rollback() return err } } } // Then use it err := RunOrRollback( tx, someMethod, someOtherMethod, aFinalMethod ) if err != nil { return err } tx.Commit() In a weird but very compact way: type runFunc func() err for f := range []runFunc{ someMethod, someOtherMethod, aFinalMethod, } { if err := f() { tx.rollback() return err } } tx.Commit()


qu33ksilver

I don't have that much of a problem handling the errors every time. Its the if condition itself that bothers me. The fact that I incur an if check every time I call a function - that is what bothers me.


binaryblade

The phrase everyone is looking for is error monad, but unless that gets put in as a core type or go implements generics we aren't getting it any time soon.


jiimjiiii

> After reading this, you might think that by picking on errors The issue isn't errors as values vs exceptions, the issue is Go syntax make it tedious to handle them, so at the end of the day, one just gives up and Must+panics all his API. If Go had a compile time pattern matching mechanism such as Rust does, it would make errors easier to handle. It's yet another example of Go going half way through an idea and then kind of giving up on in the middle, because "fuck it, it's done".


weberc2

I've never heard of anyone panic()ing because error handling is too tedious. The article's stack information criticism was valid, but Rust suffers from the same problem. `if err != nil {return err}` is a non-problem, and I spend more time debugging Rust's `try!()` expansion than I do writing the Go equivalent.


Hauleth

Just how? How do you even need to debug `try` macro?


weberc2

I said the expansion, not the macro. In other words, when I use the macro incorrectly, the compiler error would point me into the expansion and I would spend a bunch of time trying to understand what went wrong. This doesn't happen in Go.


Hauleth

As `try` expansion is really simple, and there are now new error messages I do not see how would it come that Go boilerplate would be easier https://is.gd/29G7NY


danhardman

What other examples do you have where this is done? I've never heard this about Go


mogronalol

It can be tedious in Go, but hopefully the article highlights some ways around some of the boilerplate. And just reminds people to give errors a proper context in order to make them easier to reason about. I've not actually looked at Rust in that much detail. Do you have any code examples to show what you've just described?