-
Notifications
You must be signed in to change notification settings - Fork 697
Proposal: errors' context #34
Comments
Thank you for your proposal, this idea is intriguing. While I don't think the fluent/builder syntax will fly (see #15 (comment)) the ideas of adding a structured context to an error (see the recent zap announcement by uber, https://github.com/uber-go/zap#structure) is fascinating. Taking this to its extreme would mean the stack trace is one of many possible pieces of context that could be added to an error (or error wrapper). For some context, can you explain how you think the code that is inspecting this error context data would go about using it. I'm keen to avoid any hint that this context information is a proxy for error types or sentinel values. |
Good point regarding the fluent syntax 👍 Sample usage for the context:
Package-level inspection functions would fit in nicely to avoid verbose type assertions for caller.
And another one to retrieve the value by its name for the simplicity:
Maybe it's worth to create similar functions attached to the |
Thanks for your response, there's a lot here so apologies if I don't get all to all of it in one pass.
I think this syntax has a few problems, it implies that whatever is returned from
Some of this is ok, but the parts about inspecting an error for specific values should be handled by an underlying cause that implements the appropriate behavioural interface. For the example you provided something like I'm opposed to anything that turns errors into a bag of values that are neither opaque -- as the caller expects to be able to inspect them in detail -- nor well specified -- as it is a map of string to interface{}.
This isn't clear how the caller would use this information. Do you have a sketch of some sample code that would show how it could be used ?
Possibly, although I'm not yet sold on the religion of structured logging. package errors
func Values(err error) map[string]interface{} {
if err,ok := err.(cause); ok {
return err.values
}
return nil
} This looks wrong, apart from the implementation not handling a wrapped error -- which map[string]interface{} should be returned, one applied by package errors
func Value(err error, key string) (interface{},bool) {
if err,ok := err.(cause); ok {
ret,ok := err.values[key]
return ret,ok
}
return nil,false I'm not a big fan of returning val, ok := Values(err)[key] |
No problem :) That's a big topic, better to think this through.
Maybe something like
It would be very handy, but I didn't think it through yet - it could be either a helper function in the
Storing the value in-band may not sound very good, but it's better than creating separate HTTP code value for every error for everyone (only option aside from this). This way handlers can do stuff like this:
Me neither - I would be happy with string data.
|
I think the semantics of this are pretty messy. If What I think might work better is something like package errors
func Newv(message string, values ...Value)
func Wrapv(err error, value Value, values ...Value) Where re: your suggestion of smuggling http status codes via errors, I'm not sold. Any non nil error inside http processing should be consider to be a 500, the exact last digit doesn't matter unless you're implementing a proxy. Smuggling 100, 300, and 400 codes via errors feels like violating church and state, especially the 30x status codes which need additional redirection data, and packing that into an error structure risks making a franken object, just a bag of string->interface{} data. If this is an explicit requirement you have, then there is no reason you cannot program this way today with or without this package. Here are two suggestions type HttpStatusError interface {
error
Code() int
Reason() string
} If the error implements type HttpStatusError struct {
Code int
Reason string
}
func (e HttpStatusError) Errror() string { return fmt.Sprintf("%d %s", e.Code, e.Reason) } |
Something like We want to do something like: And then on the top level we would like to have a way to recursively get all Values from an error so we could log the error to an online service like so: payload := map[string]interface{}{
"message": fmt.Sprint(logerr),
"hostname": host,
"stacktrace": fmt.Sprintf("%+v", logerr),
}
for _, v := range logerr.Values() {
payload[v.Name] = v.Data
}
logservice.Submit(payload) Is this on the roadmap? |
I'm interested in this idea; why should the message and the stack trace be the only two properties you can attach the an error? However. I am concerned about it being a vehicle to smuggle bags of unstructed data between different levels of an application as this runs counter to this package's philosophy of treating errors as opaque. |
Good reasons, and I understand that you want a tight interface. Still, adding something like Wrapv would add a lot of benefit for us. And probably others that are migrating from other log packages. |
I'm concerned you conflated these two ideas. |
Sorry, that may have appeared confrontational. My concerns is you view this package as some kind of logging package which is very much not the case. As far as |
I also realised the logging package vs error package problem just after I posted my message. :) What problem do you see with exposing it in the same way as the stack trace? type Values interface {
Values() []errors.Value
}
if err, ok := logerr.(Values); ok {
for _, v := range err.Values() {
fmt.Printf("%s:%v", v.Key, v.Val)
}
} |
But what's to prevent this being used to pass context between different layers of the application. I can easily see a developer, backed into a corner smuggling some state via errors.Wrapv and trying to recover it later with error.Values. The reason package main
import "fmt"
import "github.com/pkg/errors"
func main() {
e1, e2 := errors.New("A"), errors.New("A")
fmt.Println(e1 == e2) // false
} I know it doesn't sound very helpful, but the only way I could see |
This is an interesting discussion, especially to someone who works on structured logging packages (log15 & go-kit/log). I agree with @davecheney that exposing the context values for general purpose inspection opens a gaping hole for anti-patterns. But the idea that the context values could be used when logging is fascinating. With a structured logging package such as go-kit/log an application builds up detail in a The ideas discussed here invert that pattern. An error captured deep in the call stack bubbles up, accumulating context on the way, and somewhere higher up gets logged along with all of the accumulated context. Looking through the issue archives for go-kit/log, I found a feature request from last year remarkably similar to this discussion: Error bubbling. The issue was closed as out of scope for go-kit/log and we suggested that it should be handled by an error enhancing package. So here we are. But how could the context values be made available for structured logging without tempting people to inspect the values? |
Thanks to everyone for their comments. As I said above I'm not opposed to this feature, in fact I think it could unlock something that isn't possible today, even with languages that support exceptions. With that said, I agree with @ChrisHines that given the choice between implementing something that can be abused as a bag of data or implementing nothing , I'll choose the latter. I think even if Values was not exposed there would still be large questions on how to expose this data when an error is formatted as a string or as JSON. With that said there is nothing stopping you implementing this in your own error types today. |
After a few weeks of thinking about this I've decided that I do not plan to add this feature. While I think that an error should, in principal, be able to capture any data, and agree that the preferential treatment of the message text and the stack trace is inconsistent, I cannot think of a useful way to expose this information without leaving it open to abuse. On a related point, the goal for this package is to get it to 1.0 stage in a reasonable timeframe and propose it as a proof of concept for extending the errors package in the standard library. If that were to happen I believe that the size of this package's public API will be a limiting factor. The package currently stands at four public functions and two types which exist mainly for documentation purposes. Every symbol added to this package would make the discussion about including some or all of it in the std lib that much harder. Returning to this proposal, if there was a method to attach additional information to an error type, extracting it, or worse, manipulating it is problematic. The arguments for this are basically the same arguments for why goroutines do not have thread local variables. At a deeper level, there is no different between this type type E struct {
message string
Hostname strong
Request string
ResponseTime time.Duratio
StatusCode int
}
func (e E) Error() string { return e.message } And a type that had a method like v := error.Values(err)
fmt.Println(v["ResponseTime"], v["StatusCode"]) They both treat the error as a unique value, which is not the design goal of this package. For these reasons I do not plan to add this feature. Thank you for your understanding. |
print cause when there is one
Hello,
What do you think of errors' contexts like
net/context
or simplemap[string]interface{}
stored with the original error?It would be really handy; example:
this way the imaginary http handler won't need to know about ErrNotFound; it would just grab the value from the context. It could be even better:
however, this approach would lose stack data; it would require the stored stack's reset.
The context or map can be initiated on demand (when adding first value), so it wouldn't create some significant overhead if context is not used.
The text was updated successfully, but these errors were encountered: