5 ms·
In the last few months I've realized what I desperately need: a way to wrap an error with a call stack at the point where it enters our code base. This would pr
by physicles 3y ago
In the last few months I've realized what I desperately need: a way to wrap an error with a call stack at the point where it enters our code base. This would probably save me on average 20-30 minutes a week.
I see this all the time:
main.go:141 error: could not transmogrify the thing: a144cd21c48
And then I literally grep the code base to find the error message. That works ~50% of the time, but the other 50%, I see this:
main.go:141 error: not found
And then I have to spend 5-10 minutes spelunking to try to find where that error might have originated from.
But this would be amazing:
main.go:141 error: not found callstack=...
- tombh 3y agoThis is such an infuriating problem. I'm convinced I'm using Go wrong, because I simply can't understand how this doesn't make it a toy language. Why the $expletive am I wasting 20-30 and more minutes per week of my life looking for the source of an error!? Have you seen https://github.com/tomarrell/wrapcheck https://github.com/tomarrell/wrapcheck? It's a linter than does a fairly good job of warning when an error originates from an external package but hasn't been wrapped in your codebase to make it unique or stacktraced. It comes with https://github.com/golangci/golangci-lint https://github.com/golangci/golangci-lint and can even be made part of your in-editor LSP diagnostics. But still, it's not perfect. And so I remain convinced that I'm misunderstanding something fundamental about the language because not being able to consistently find the source of an error is such an egregious failing for a programming language.
- randomdata 3y agoI find it interesting how, as soon as the word error shows up, people seemingly forget how to program. Ignore the word error for a moment. Think about how you program in the general case, for a hypothetical type T. What is it that you do to to your T values to ensure that you don't have the same problem? Now do that same thing when T is of the type error. There is nothing special about errors.
- kitd 3y agoAgreed. In fact I wrote a (very) small library to help deal with it. https://github.com/kitd/chock https://github.com/kitd/chock
- randomdata 3y agoIts flaws or merits aside, when you have no other useful context to add to the error, that's precisely what Errorf is for. func bar() error { err := baz.Transmogrify() return fmt.Errorf("transmogrify: %w", err) } func foo() error { err := bar() return fmt.Errorf("bar: %w", err) } func main() { err := foo() fmt.Printf("foo: %v", err) // foo: bar: transmogrify: not found } There's your callstack, without the cost of carrying around the actual callstack.
- physicles 3y agoIndeed, our code base is littered with fmt.Errorf("...: %w", err), but that only works if enough places in the code add context. Currently only about 15% of return sites do this. And I disagree that the cost of carrying around the callstack is something to worry about. Errors are akin to exceptions in C++/Java: no happy path should rely on errors for control flow (except io.EOF, but that won't generate a call stack). They should be rare enough that any cost below about 1ms and 10k is negligible.
- morelisp 3y agoAre you suggesting it's OK if ParseInt failures take 1ms? Or should ParseInt use a different "kind of error" that's not commensurate with the regular error kind? Do you think most errors look more like ParseInt, or more like sql.Open where 1ms might be acceptable? (Do you think a call stack from the insides of sql.Open would be useful? My experience, mostly not...) So the stacks should probably only be for "complex errors", and only for frames that happen in code you (hand waving) "care about". Maybe your programs just have far too complex internal error handling?
- physicles 3y agoSee my response to a sibling. I wasn't clear; I was implicitly differentiating between these: 1. errors that can be handled locally (such as parsing; in other languages, these situations are often signaled with return values instead of exceptions) 2. errors that can't be handled locally (such as network errors; other languages use exceptions for these) My argument was that worrying too much about error handling performance in #2 is premature optimization. 1ms is extreme, but the actual figure of capturing a call stack in Go -- several microseconds, by my benchmark -- puts it squarely in the "don't worry about it unless your code is performance-critical" category.