New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: Go 2: counter proposal to error values #30350
Comments
What stands out to me most is the serialization API is disjoint. In particular, it’s not immediately clear how to use it (unlike in the original proposal) and requiring I like the idea of the I like how Having to replace To me, the features it provides makes it seem more like a third-party library than something I’d expect in the stdlib. |
Thanks for the feedback, @ericlagergren
For how to use it, see test file in the example package. Basically a printer is initialized with a serializer, then use The
Unfortunately the moment you add error wrapping to the std library and these wrapping does not allow to recover the individual error message (without the wrapped ones), this can't be achieved. This is not possible as a third-party library alongside the official proposal.
An interface because frame testing is very complicated (and extremely brittle) as a struct. I can be made a struct but bare in mind that.
|
@ianlancetaylor I should have made the distinctions clear. I'll do it here: Formatter(original) vs Serializer(this) Is and As(original) vs Last(this) Frames Performance Migration |
I understand the motivation for I don't think you can conclude from the fact that we only provide As you mentioned, we've discussed Lastly, I want to point out that |
Thanks for the feedback @jba I don't quite follow 100% of your first paragraph (mind clarifying the
Not format it in a custom way, but by default it would use the
Problem is, I don't think that enough. The normal, general, logical style may be "{err}: {err}", but for some purposes the error is not meant for human but machine use and may want frames only, or in JSON form, etc. I don't think a 'one format fits all' is realistic, so the error itself should not be committed to a format at all. Re
My point is
|
Nothing new from me. |
Addressing a few specific points:
Under #29934, errors that implement a
This is an unsupported assertion. It is also unclear what specific operations this is referring to.
This strikes me as untenable. |
Thank you for your attention @ianlancetaylor @jba @neild @ericlagergren I am re-designing some parts of this proposal based on ideas shared in the main proposal discussion and other feedback received, so do not spend any unnecessary time on this one. Answering @neild : Fomatter -> my point is it is the
True, but I don't have bandwidth to define benchmarks for #29934 , would be great if some of the owners did. The operations I would like to see benchmarked are wrapped error creation (including any fmt-%w logic and frame capturing) and serialization of wrapped errors (
I've come to agree. It is the main aspect I am trying to fix on the next iteration |
You cannot change the meaning of Error() string. That is an existing API.
What @neild means is that there are benchmarks. It is unclear what you mean. One of the issues errors.Printer and errors.Formatter address is allowing localization of errors. I'm unclear how this is addressed in this design.
Printer was designed to allow for custom implementations, for instance supporting localization or converting the error chain into a structured form. There is a lot of semantics reflected in the interfaces of proposal 29934. For instance, it maintains two error chains: one for semantics and one for detailed analysis purposes only. The APIs for both are deliberately very different to ensure there remains little confusion as to which is which. Another example is ability to localize errors through the Printer interface (see how x/text, and other localization frameworks, allows localized formatting of strings and how this is reflected in the Printer API). This proposal seems to largely discard these semantics. Unless I'm missing something. It would be good to see some examples of writing an error works in this proposal. I have to admit I don't fully understand it. |
About the differences:
This can be done in the original proposal by defining another printer.
Why? It is quite easy to write custom errors in the original proposal.
In your proposal, xerrors.New does not include stack information. Only wrapped errors do. How would you add stack information in errors.New or for any error that does not wrap an error? It is not hard to use it in the original proposal: struct MyError {/* custom parameters ; */ frame errors.Frame} Assuming that one has to write an additional FormatError, this is about the same work. Having to add a FormatError and Unwrap method can be tedious. But nothing in the original proposal prohibits one from writing a similar pattern: // type in some lib somewhere Then: struct MyError {/* custom parameters ; */ x.Wrapped} This is even shorter (sure you can do that with your proposal too, but that is not the point.
Did you compare the benchmarks against the proposal implementation in Core? It seems you're benchmarking the printing, not the wrapping. It is actually the wrapping that is the interesting thing to measure. BTW, the original proposal allows for a significant performance gain in Errorf in many cases, this allows us to amortize the cost of storing the frame somewhat in these cases. Your implementation seems to do quite a bit of work for wrapping.
Why is this different? The Unwrap function is identical. Is can be used for easy comparison. Admittedly, informative-only errors are harder to navigate, but this was intentional to avoid having users rely on them for programmatic decisions. We omitted Cause as we thought Is is the preferred idiom (and semantically more correct).
I agree with this difference. Relying on the redefinition of Error() is a deal breaker, though. Have you thought about how to adapt your design to not do that? Other observations: How would printing work in conjunction with fmt functions? Many existing packages, like pkg/errors, rely on the %+v format to work. The original proposal allows for such packages to be adapted to use the proposed interfaces and keep working. Is there a way to print detail information other than stack frames? I didn't see it but might have missed it. |
Thanks for the feedback @mpvl
Just highlighting that since the answer to some of the below is 'yeah, needs improvement, I'm working on it'
I just mean this discourages string errors by not having wrapping support via Printf(...%w). The original, by supporting wrapping via Printf, naturally makes string errors more accessible. This ties in with Formatter/Printer vs Serializer, see below.
Formatter/Printer vs Serializer - I give you a scenario: Formatter/Printer - a separate concern - I give you a scenario:
Correct, only wrapped errors have stack information (or any other wrap-related functionality). The idea was to create xerrors.New or other possibly non-wrapping errors, then if adding more context via Wrapper-implementing errors. This is being improved on the next iteration.
Sorry, been looking at the original proposal not golang master. Seen the benchmarks, will make sure to make comparable benchmarks.
(regarding my proposal)
Yes 😄, should be posting that very soon
For each separate printing form (%s, %v?, %+v, ?) a Serializer needs to be implemented that satisfies that format, and be called explicitly by Printf-like functions. Should be no different output than it is now if the old errors satisfied some pattern (such as "... : %s", err) that the Serializer can replicate. Having said that, since migration to wrapping is not automatic, the main thing here is when updating some error to wrapping, make sure the new wrapped format matches whatever output the un-wrapped produced, or face breaking %s, etc. comparisons.
As it stands, no, but there is nothing special about frames other than they are added by default by the standard library and the way the default serializer handles them. If you don't use these default implementations you could use completely custom stack formats, or whatever it is you may need. If you managed to read up to here - thank you! |
I did :) So who would implement custom serializers? I'm still quite unclear about the concepts and who would implement what. It seems that custom formatters must have explicit knowledge of the errors it handles I don't understand how such an approach would scale.
How would a serializer have access to such patterns? These patterns are usually gone by the time an error is created. I agree that it is useful to keep these (i.e. for localization), but even changing Errorf to store those would be a breaking change. Actually, storing err as we do know is not fully backwards compatible, but that is unlikely to cause problems. The Printf method of Printer allows for errors to convey such structure, still, in a generalized way so that text extractors can pick it up for translation.
How is that scalable? I still think I'm not grasping your proposal. Clearly a Serializer cannot be expected to know of all errors. I must be missing something. Note that in the original implementation information about the error chain is available as well, and thus one could write a formatter that selects on error types. But generally it would be infeasible for a formatter to know about all errors. It doesn't seem like a practical avenue. Maybe things will be clearer to me once this design no longer assumes the semantics of the Error() method changes. |
Custom serializers are meant to be much rarer (and harder to produce) that custom errors. I'd imagine besides the default serialiser ("{err}: {err}: ...") only a handful of serialisers will be produced and shared widely, for example with the below functionalities:
A good reference is zapcore.Encoder, from which I draw much inspiration. The other category of serialisers are those more tied to whatever libraries you are using, unlikely to be shared as widely. These likely rely more on
yes, some serialisers may have to have some explicit knowledge of the errors it handles.
Note also this custom functionality is best supported via interfaces, not types matching. To facilitate a scalable approach and easier integration of errors in 3rd party libraries, it would be advisable to encourage some minimal standards amongst errors such as having certain method(s): Also all errors (even private ones) should have some public method through which one can, if need to, identify them and either drop them or access their raw data. This is particularly significant for 3rd party error types, since one can't know what users will want to do with the error down the line.
I can't understand what you mean here, sorry. What I am trying to say though is that there is no automatic wrap error upgrading - adding these changes, in itself, makes no change whatsoever. When users begin to upgrade their |
@ianlancetaylor @ericlagergren @jba @neild @mpvl As promised, please see the revised proposal #31111. It preserves many features from this one, the main improvement is that |
Thank you for all the feedback, I am closing this issue in favor of the new one #31111 |
See #31111, the second iteration of this ideas.
This is a counter-proposal to the Go2 error values proposal only.
That proposal is referred throughout as the 'original proposal'.
Please review the original proposal, this assumes knowledge of it and of the discussions around it, from which this takes many ideas.
Code available in https://github.com/JavierZunzunegui/Go2_error_values_counter_proposal
Packages
xerrors/
: the proposed code changesxerrors/xserialiserexamples/
: contains some examples used to demonstrate how these changes may be used, but is not meant to be merged.Overview
This introduces wrapped errors in a similar manner to the original proposal but with significant differences:
for error {
"foo"
} wrapping {"bar"
} wrapping {"cause"
}, can produce:"foo - bar - cause"
or"foo: bar: cause"
or"{"foo":{"bar":{"cause"}}}
or...
struct MyError {/* custom parameters ; */ errors.Wrapping}
func (err *MyError) Error string {...}
func NewMyError(wrap error) error {return &MyError{Wrapping: NewWrapping(wrap)}
(See benchmarks)
A comparison of original vs this proposal in #30350 (comment)
In more detail:
Wrapper
Identical to the original proposal, a new interface
Wrapper
extendingerror
with anUnwrap() error
method a helperfunc Unwrap(error) error
.Error() string
The meaning of error's
Error() string
is conceptually different to that of the original proposal.In the original, it returns both the target error's message as well as that of all the wrapped errors.
In this one,
Error() string
only returns the message for the target error and not that of the wrapped ones. To print a full error message aSerializer
is required, see below.Serializer
A new interface, it is responsible for the key logic used in serializing errors, including wrapped ones.
In some regards it is similar to the
Formatter
andPrinter
interfaces of the original proposal,but unlike
Formatter
it is not implemented by the error.It has 4 methods:
Keep
: defines what errors this serializers will print and which ignoreCustomFormat
: allows a way to serialize individual errors in a way other than the defaultError() string
Append
: combines the messages of the different wrapped errorsReset
: implementation detail to reduce memory allocationsPrinter
A struct that wraps a
Serializer
. It mainly exists to remove some otherwise repetitive logic fromSerializer
and optimise errors serialization, particularly with regards to heap allocation viasync.Pool
.String, DetailString, Bytes, DetailedBytes functions
Methods using the default serializer (the popular "{err1}: {err2}" format) for use in %s/%v formatting.
The Detailed forms additionally print frame information if available.
Wrapping, NewWrapping and FrameError
Embedding
Wrapping
in errors provide a very easy way for custom errors to implementWrapper
.NewWrapping
transparently provides frames support to custom errors which use embeddedWrapping
.Frames support is almost identical (and largely copied) to that in the original proposal except it is not a property of the error ('detail') but a separate error,
FrameError
.Last
Last
is used to navigate the error wrapping chain and identify errors of interest. It serves the same purposes asAs
andIs
in the original proposal. The comparison between these has already been discussed in detail in the original's discussion.Similar and Contains functions
Replacement for
reflect.DeepEqual
comparison of errors. Both ignore wrappedFrameError
.Migration & Tooling
Preliminaries: Error() to String(error); reflect.Equal to Similar
While the introduction of this new error package does not break anything in itself, it's use by imported libraries will not interact well with some code written before these changes. Two things in particular need to be addressed: the meaning of
error
'sError
method, and comparing errors.Error() string
currently represents the whole error message, as it does in the original proposal.In this one it does not, it requires an external
Serializer
to do so, or the new standardString
method. All printf-like methods in the standard library must be updated to useString(err)
instead oferr.Error()
for error's %s representation, but even then if user's callError()
explicitly this will not have the expected behaviour once the libraries they depend on stand returning wrapped errors.Therefore before the main changes may be landed, a mocked
String
method should be added to the errors package and vet encourage users to use it instead ofError()
. In other words,fmt.Println(err.Error())
will eventually become problematic and should be changed tofmt.Println(errors.String(err))
(or simplyfmt.Println(err)
). If not obvious, the initial implementation ofString
will befunc String(err error) string {return err.Error()}
.Error comparison has been brought up recently in the main proposal's discussion.
Because
reflect.DeepEqual
will not work well with wrapped errors and frames, these need to be migrated to use the newerrors.Similar
orerrors.Contains
. Again, this should be done before the main migration, initially with a mock implementation being just a wrapper ofreflect.DeepEqual
. The vet tool should also be updated to push this change.Main migration
Once the preliminary changes are considered broadly implemented, the actual proposed errors package can be added to the standard library. Libraries which did not follow the preliminary steps could find themselves printing incomplete error messages or getting false negatives when comparing errors for equality.
Other Remarks
fmt.Errorf and Wrapf
A stated objective of the original proposal is to automatically make
fmt.Errorf
'just work' in the wrapped form, and integrate it with frame just as easily. This proposal takes the opposite approach -fmt.Errorf
remains unchanged, does not use the wrapping pattern and should be considered obsolete (and it's use discouraged by vet).Similarly, I have not added a default
Wrapf
method because I think it should be seen as an anti-pattern. Instead, typed errors are encouraged so thatSerializers
may filter them out or customize their output. Either way,Wrapf
is really justWrap(err, fmt.Sprintf(...))
so it is hardly a blocker.Code generation and Last
The
Last
pattern has been discussed in detail in the original proposal. One aspect brought up is that to get a typed error one has write some custom helper functionfunc(error) bool
and type-assert the output value. While this can be done throughgo generate
, I consider it unnecessary and not part of this proposal, but nonetheless if you want to review that you can read this feedback to the original proposal.The text was updated successfully, but these errors were encountered: