Proposal Details
I propose that log/slog
introduces a function to create an error from a string message and zero or more slog.Attr arguments. The type, implementing the error interface, would also implement the LogValuer one as well, in order to render the error as an attribute group when logged.
Rationale
An error, that acts as an attribute group when used with slog
can currently be created by a third party. There are no known problems that should prevent this from happening. The only reason this error should exist in the standard library is adoption.
As a third-party dependency, one's own code and errors can benefit from such a structured error representation. However, the errors produced by dependencies would not. They would also have to depend on such a third-party module as well.
If this functionality would be introduced into the standard library, the above scenario would not necessarily be different. However, it is more likely that more modules would embrace it as it is provided by the language itself. I believe adoption would be significantly faster because of this.
Finally, such an addition would not be unprecedented. Such a function should be as familiar as the way we currently create formatted errors, through the fmt.Errorf
function.
Language change
I proposal the following code, or one that achieves similar results to be added to the log/slog
package:
package slog
import (
"strings"
)
const CauseKey = "cause"
type serror struct {
msg string
err error
attrs []Attr
}
// Error implements error.
func (s serror) Error() string {
var b strings.Builder
_, _ = b.WriteString(s.msg)
if s.err != nil {
_ = b.WriteByte(' ')
_, _ = b.WriteString(CauseKey + "=[" + s.err.Error() + "]")
}
for _, attr := range s.attrs {
_ = b.WriteByte(' ')
_, _ = b.WriteString(attr.String())
}
return b.String()
}
func (s serror) LogValue() Value {
size := len(s.attrs) + 1
if s.err != nil {
size++
}
attrs := make([]Attr, 0, size)
attrs = append(attrs, String(MessageKey, s.msg))
if s.err != nil {
attrs = append(attrs, Any(CauseKey, s.err))
}
attrs = append(attrs, s.attrs...)
return GroupValue(attrs...)
}
func (e serror) Unwrap() error {
return e.err
}
func Error(msg string, attrs ...Attr) error {
return serror{msg: msg, attrs: attrs}
}
func WrapError(msg string, err error, attrs ...Attr) error {
return serror{msg: msg, err: err, attrs: attrs}
}
In this example, I've introduced two functions, Error
to create an initial error, and WrapError
to wrap an existing one.
Alternatively, a slog.Error
attribute function may be introduced instead of the WrapError
, to allow wrapping one or more errors. This would be similar to fmt.Errorf
, and like it, would require different types to implement the different Unwrap
interfaces.
Comment From: apparentlymart
Interesting!
Is your hope then that third-party libraries would begin using this error type just in case some caller might want to pass the error directly to logging? If this were accepted, would you hope for it to become idiomatic for all errors that might possibly be logged to be LogValuer
implementations?
It could be more orthogonal to introduce some means of associating attributes with an error that isn't so tightly bound to slog
's idea of attributes and then teach slog to understand that more general convention, but I can't argue that it isn't far easier to just borrow the concept of attributes that package slog
already defined. 🤔
Comment From: seankhliao
This doesn't fit in to the design of the standard library's error handling strategies, which are centered around matching with equality and Is / As.
I believe this falls into the category of https://go.dev/doc/faq#x_in_std