story
What eventually became the standard library error wrapping proposal evolved from the work done on the Upspin project. It did include stacktraces, and believed like you that it would be useful to have them. But analysis of the data showed that nobody ever really used them in practice and, for that reason, was removed from the final proposal.
> particularly given the most popular package doing that previously is now unmaintained.
Lacking wide appeal doesn't mean there isn't a niche need, of course. However, with the standard library accepting a standard for error wrapping, which this package you speak of has been updated to be compatible with, what further maintenance would be needed, exactly? It would be more concerning if it wasn't considered finished by now. It seems the solution for niche needs is right there.
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=...Have you seen 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 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.
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.
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. func bar() error {
err := baz.Transmogrify()
return fmt.Errorf("bar: %w", err)
}
func foo() error {
err := bar()
return fmt.Errorf("foo: %w", err)
}
func main() {
err := foo()
fmt.Printf(err)
// foo: bar: transmogrify: not found
}
Also, I tend to skip quite a lot of layers. The (only?) advantage of manual wrapping over stack traces is that a human can leave just 3 wrappings which are deemed sufficient for another human, while stack trace would contain 100 lines of crap. func bar() error {
err := baz.Transmogrify()
return fmt.Errorf("bar: %w", err)
}
This is broken. If baz.Transmogrify() returns a nil error, bar will return a non-nil error.Also, annotations like this, which repeat the name of the function, are backwards. The caller knows the function they called, they can include that information if they choose. Annotations should only include information which callers don't have access to, in this case that would be "transmogrify".
The correct version of this code would be something like the following.
func main() {
fmt.Printf("err=%v\n", foo())
}
func foo() error {
if err := bar(); err != nil {
return fmt.Errorf("bar: %w", err)
}
return nil
}
func bar() error {
if err := baz.Transmogrify(); err != nil {
return fmt.Errorf("transmogrify: %w", err)
}
return nil
}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.
> 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.
This may be true in C++ or Java, but in Go, it is absolutely not the case.
Errors are essential to, and actually the primary driver of, control flow!
Any method or function which is not guaranteed to succeed by the language specification should, generally, return an error. Code which calls such a method or function must always receive and evaluate the returned error.
Happy paths always involve the evaluation and processing of errors received from called methods/functions! Errors are normal, not exceptional.
(Understanding errors as normal rather than exceptional is one of the major things that distinguish junior vs. senior engineers.)
It would be a bit odd to not add context, wouldn't it? Same goes for any value. This is not exclusive to errors. If you consider a function which returns T, the T value could equally be hard to trace back if you find you need to determine its call site and someone blindly returned it up the stack. There is nothing special about errors.
While ideally you are returning more context than Errorf allows, indeed, it is a good last resort. If your codebase is littered with blind returns, the good news is that it shouldn't be too hard to create a static analyzer which finds blind returns of the error type and injects the Errorf pattern.
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?
See my sibling comment: https://news.ycombinator.com/item?id=37234455