go: proposal: Tracing errors with the `tracer` interface.

The error system is great, but sometimes it’s hard to figure out where an error came from. Here’s a proposal that could more or less be implemented today with a Golang source code transpiler, but would benefit from official Golang support. This is a proposal for Go1. (For Go2 I would rather propose that error be a tracer itself)

The proposal is to support the following:

  • a global interface like type terror interface {} (name can change)
  • in "pkg/errors", func Terror(e error) terror {...} as implemented below (name can change)
  • in "pkg/errors", keep the same signature in errors.New() error {...} but have it return a terror that can be run-time converted.
// New global type.
// Like `error`, a global interface.
// Trace calls `runtime.Caller` to get the file and line number.
type tracer interface {
    Trace()
}

// New global type.
// A new type of error that traces.
type terror interface {
    error
    tracer
}

// In pkg/errors.
func Terror(e error) terror {
  if t, ok := e.(terror); ok {
    return t
  }
  return newTerror(e)
}

// In pkg/errors.
// Same method signature, but actually returns a terror.
func New(...) error {..}

// Usage:
var e error = errors.New("bark")
var t1 terror := e // compile error
var t2 terror := e.(terror) // ok
var t3 terror := errors.Terror(e) // ok

Everything else can be handled by third-party libraries and code transpilers. What follows is just one example of syntax support from a transpiler with support for an assignment modifier !.

func foo() terror {
  var x terror = errors.New("bark").(terror)
  var x! terror = x // trace!
  var y error = x
  var z! error = x // compile error
  return x
}

x := foo()
x! = x // trace!
x! = <-ch // trace!

var y, z *terror
y = z
y! = z // trace! (but is this what we want?)
*y = x
*y! = x // trace!

type MyStruct struct {
  t terror
}
s := MyStruct{x}
s = MyStruct{field!:x} // trace!
s2 := s
s2! := s // compile error s.(tracer) is not ok

var i interface{}
i = x
i2 := i.(terror)
i3! := i.(terror) // trace!

func bar(t! terror) {...}
bar(x) // trace @ bar()!

The pain-point with error is that it doesn’t trace. This seems like a way to inject tracing with minimal syntactic change and good performance.

The transpiler could also work without explicit !, but rather automatically every time something is assigned to a var _ terror.

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 5
  • Comments: 17 (5 by maintainers)

Most upvoted comments

@as, the fundamental problem that made me adopt pkg/errors (despite it’s warts) is that simply finding the original errors.New("some unique error message") call still tells me nothing at all about how the code ended up there. While I can (manually) add fmt.Errorf("unable to do whatever I was attempting: %s", err) to all my if err != nil` blocks, now I have a different error than I started with, so I can’t test for it. Sure, I can create my own custom interface for every error I need to test for, but that’s a significant amount of extra work, and it doesn’t guarantee uniqueness since any other package you’re using might have used the same interface definition to signify some other specific type of error.

I believe one of the fundamental issues here is that Go assumes all errors must be super-high performance. Certainly, hot pieces of code need that ability, but for many projects the amount of performance-critical code is dwarfed by all the ancillary stuff.

@nhooyr I looked to find some at some point, but it’s spread out a lot through numerous proposals on the pkg/errors issues, and can be difficult to find any specific examples. It has however been more of a vague, (paraphrased) “your proposal is good, but a lot of people rely upon the API as currently designed… and we would cause great difficulty to lots of people in the event of a fundamental redesign of the API… no matter how noble it might be.”

So, much more like the Cancel <-chan struct{} in the net.Request struct…