-
Notifications
You must be signed in to change notification settings - Fork 18.5k
Description
In Go many functions return an error result. It is easy for the caller to ignore these results. This has led to various proposals to check that all errors are handled (e.g., #20803, #27845).
One problem with these proposals is that it is always OK to ignore some errors. For example, bytes.(*Buffer).Write never returns a non-nil error result. The only error returned by fmt.Fprint is from a call to Write. Therefore, using fmt.Fprint with a bytes.Buffer can never return a non-nil error. A proposal that requires that code always check or explicitly ignore the error result of fmt.Fprint would be inappropriate when used with a bytes.Buffer, even if it might be appropriate when used with a os.FIle.
A slightly more subtle example is bufio.Writer. Its Write method can return a real error result. By design, though, that error result is not only returned, it is also cached. Any use of bufio.Writer must eventually call the Flush method. The Flush method will return any cached error. So it is safe to ignore all errors from bufio.(*Writer).Write as long as the program checks the error returned by Flush.
To put it another way, in Go some errors can be safely ignored, but whether that is true at a particular call site is not a property that can be statically determined. Requiring that all errors be handled would not only break many existing Go programs, it would introduce additional code that would never be necessary and in some cases would never be executed.
Since static detection is difficult, I want to raise the possibility of a dynamic approach. I don't know whether this is really a good idea, but it seems that it might be an interesting design space to explore.
The ignored error detector is a rewriting approach, like the race detector. It could be implemented directly in the compiler, or it could be implemented as a source-to-source rewriting tool invoked by the go tool.
We introduce a new type in some internal package with an Ignored method:
type IgnoredError struct { error }
func (ie IgnoredError) Ignored() { panic("ignoring possible real error") }
func (ie IgnoredError) Unwrap() error { return ie.error }
func MakeIgnored(err error) error {
if err == nil {
return MagicNilError
}
if _, ok := err.(IgnoredError); ok {
return err
}
return IgnoredError{err}
}
func MakeNotIgnored(err error) error {
if err == nil {
return nil
}
if ie, ok := err.(IgnoredError); ok {
return ie.error
}
return err
}
var MagicNilError IgnoredError{nil} For any function or method that returns an error result, we check where the returned errors come from. If an error is returned from a call to errors.New or fmt.Errorf, and that error is not stored anywhere other than a local variable before being returned, then we change the return to return MakeIgnored(err). If an error is returned unmodified from some other function call, we don't change the code; we just return the unmodified error. If the function returns an explicit nil for the error result, we change it to return MagicNilError.
For any comparison of a value of type error with nil, we rewrite err == nil to MakeNotIgnored(err) == nil (and similarly for !=, of course).
For any call to a function or method that returns an error, if that error is not stored anywhere--not stored in a local variable, not assigned to _, but simply ignored entirely--we change the code to
..., err := F()
var ie IgnoredError
if errors.As(err, &ie) {
ie.Ignored()
}I'm sure I'm missing some cases, but I hope that the general idea is clear.
The effect of this will be that for any call to a function or method that can in some cases return a real error, if that error is ignored entirely, the program will panic. In conjunction with a testsuite with good coverage, the ignored error detector should find most of the cases where a program is ignoring an error that might actually be serious.