Go 2 Draft: Error Values

dean

dean

Posted on September 3, 2018

Go 2 Draft: Error Values

Go 2 is being drafted.

If you haven't heard, there will be a few major language changes coming to Go 2. The Go team has recently submitted a draft of these changes, and as part of a 3-part series, I've been going through each part, explaining it, and state my opinion on them! This is the second part of the series. Last time I did error handling, and this time I will be discussing error values!

Remember that these are just drafts. They will most likely be added to the language, but there is still a chance that it won't. Also, remember that these drafts are not final, of course. The syntax will probably be a bit different from what it is right now.

These may not also be the only changes to the language, but these will probably be the only major changes.

I thought you already discussed errors!

Well, the first part was about error handling, and a new syntax which aides in handling errors. This part is about the improvements to the error interface and the errors package. There's some really good improvements that may be coming!

Okay, so what's wrong with the errors package?

There's two main issues: Inspecting errors, and Formatting errors.

Inspecting errors

Currently, there is no way to get context for an error without a custom implementation. So there's no provided way to provide context to an error you are returning, or to see if there was a cause for an error that you've received.

To fix this, the following may be defined in the errors package:

package errors

// Describes an error which wraps another error
type Wrapper interface {
    Unwrap() error
}

// Reports whether err or any of the errors in its chain is equal to target.
function Is(err, target error) bool

// Checks whether err or any of the errors in its chain is a value of type E.
// If so, it returns the discovered value of type E, with ok set to true.
// If not, it returns the zero value of type E, with ok set to false.
func As(type E)(err error) (e E, ok bool)

// NOTE this uses the syntax from the generics draft:
// https://go.googlesource.com/proposal/+/master/design/go2draft-generics-overview.md
Enter fullscreen mode Exit fullscreen mode

Wrapper is an interface which describes an error which wraps another error. This forms a singly-linked list (or "chain") of errors, with the head as the most general error ("could not initialize config") and the tail as the root cause of the error ("file not found").

Is is the equivalent of the old if err == pack.ErrSomething, but it checks the entire chain of errors. This way, you could do if errors.Is(err, os.ErrNotExist) and it would tell you if at any point the error was caused by a file not existing.

As is the equivalent of the old smth, ok := err.(pack.ErrSomething), but it also checks the entire chain of errors. Notice that the As function uses generics, which I will cover in my next blog post.

Anyway, As's usage would look something along the lines of errne, ok := errors.As(*os.ErrNotExist)(err). I personally don't like the double-parentheses syntax for generics (to be honest, I don't like generics that much in general...)

If generics are not yet in the language, a good substitute would be

var errne *os.ErrNotExist
errors.AsValue(&errne, err)
Enter fullscreen mode Exit fullscreen mode

Formatting errors

Also, when viewing printed errors, there is often very little information! Printing the error gives something like "can't read config" or "invalid path ", and no good way to get more detail.

To address this, the following may be added to the errors package:

type Formatter interface {
    Format(p Printer) (next error)
}
Enter fullscreen mode Exit fullscreen mode

Formatter is an interface that describes an error which may be formatted. Printer is an interface which contains the functions Print and Printf, along with a Detail function. The Printer is provided package that is printing the error (usually fmt if printing via fmt.Print...()).

Then, fmt.Errorf() could then be improved to return an error implementing Formatter. It is also suggested that if the format string ends with : %v or : %s that it should return a Wrapper as well, thus improving the returned error even more.

Errors with custom formats could look like:

// Implements error and errors.Formatter
type ConfigError struct {
    File     string       // A human-readable name for this config
    Source   string       // The source file and line this error occurred on
    CausedBy error        // The error this was caused by
}

func (e *ConfigError) Init(file string, cause error) {
    e.File = file

    if _, file, line, ok := runtime.Caller(1); ok {
        e.Source = fmt.Sprintf("%s:%s", )
    } else {
        e.Source = "error source unavailable"
    }

    e.CausedBy = cause
}

func (e *ConfigError) Format(p errors.Printer) (next error) {
    p.Printf("error reading config %s", e.File)
    if p.Detail() { // if printing with `%+v` rather than just `%v`
        p.Printf("Perhaps incorrect file name or invalid format?")
        p.Print(e.Source)
    }
    return e.CausedBy
}

func (e *ConfigError) Error() string {
    return fmt.Sprint(e) // `fmt` will call the new Format function
}
Enter fullscreen mode Exit fullscreen mode

And then when it's printed, it'll be something like...

fmt.Printf("%v", err)

error reading config MyConfig

===

fmt.Printf("%+v", err)

error reading config MyConfig:
    Perhaps incorrect file name or invalid format?
    github.com/deanveloper/config:1337
--- file does not exist
Enter fullscreen mode Exit fullscreen mode

Nifty!

Putting these both together

Now, let's add one more function to our ConfigError in order to complete it. Let's add our Unwrap function so that we can use errors.Is and errors.As with it!

// Implements error, errors.Wrapper, and errors.Formatter
type ConfigError struct {
    File     string       // A human-readable name for this config
    Source   string       // The source file and line this error occurred on
    CausedBy error        // The error this was caused by
}

func (e *ConfigError) Init(file string, cause error) {
    e.File = file

    if _, file, line, ok := runtime.Caller(1); ok {
        e.Source = fmt.Sprintf("%s:%s", )
    } else {
        e.Source = "error source unavailable"
    }

    e.CausedBy = cause
}

func (e *ConfigError) Format(p errors.Printer) (next error) {
    p.Printf("error reading config %s", e.File)
    if p.Detail() { // if printing with `%+v` rather than just `%v`
        p.Printf("Perhaps incorrect file name or invalid format?")
        p.Print(e.Source)
    }
    return e.CausedBy
}

func (e *ConfigError) Unwrap() error {
    return e.CausedBy
}

func (e *ConfigError) Error() string {
    return fmt.Sprint(e) // `fmt` will call the new Format function
}
Enter fullscreen mode Exit fullscreen mode

And now we can use this error (note, I am using syntax from the "error handling" Go 2 proposal. I have a blog post on that one already)


func main() {
    handle err {
        if cfgErr, ok := errors.As(*config.ConfigError)(err); ok {
            if errors.Is(err, os.ErrNotFound) {
                // Nice, clean output if we couldn't find the file
                log.Fatalln("Could not find config file %s", cfgErr.File)
            }
        }
        // Hmm, let's get a more verbose output.
        // Maybe a parsing error?
        log.Fatalln("%+v", err)
    }

    check config.Create("MyConfig")
}

Enter fullscreen mode Exit fullscreen mode

My Suggestions

In terms of errors.Is, I'd really like a way to check multiple errors.

Perhaps instead of errors.Is(root, cause error) bool we could have errors.IsAny(root error, causes ...error) error (terrible name, but the semantics are what's important), which we could then use a switch err on. It's very frustrating to not have an efficient way to check multiple types without looping ourselves.

Also, I think this is adding a lot of complexity to error values. To me, one of the things special about Go was that errors were simple. I like the Wrapper idea, along with errors.Is and errors.As, but the Formatter I think is a bit much. Maybe add a type Detailer interface { Detail() string } to errors and let fmt take care of the actual formatting, as pretty much all Format functions will be pretty similar...

func (e *MyErr) Format(p errors.Printer) (next error) {
    p.Print(/* short error string */)
    if p.Detail() {
        p.Print(/* details */)
    }
    return /* caused by */
}
Enter fullscreen mode Exit fullscreen mode

Wait a second, all the things I wrote placeholders for could just be functions!

If Go just had an errors.Detailer interface, then our ConfigError would look like...

// Implements error, errors.Wrapper, and errors.Detailer
type ConfigError struct {
    File     string       // A human-readable name for this config
    Source   string       // The source file and line this error occurred on
    CausedBy error        // The error this was caused by
}

func (e *ConfigError) Init(file string, cause error) {
    e.File = file

    if _, file, line, ok := runtime.Caller(1); ok {
        e.Source = fmt.Sprintf("%s:%s", )
    } else {
        e.Source = "error source unavailable"
    }

    e.CausedBy = cause
}

func (e *ConfigError) Detail() string {
    return "Perhaps incorrect file name or invalid format?\n" + e.Source
}

func (e *ConfigError) Unwrap() error {
    return e.CausedBy
}

func (e *ConfigError) Error() string {
    return "error reading config " + e.File
}
Enter fullscreen mode Exit fullscreen mode

And then fmt would be able to print each error in the chain.

All in all...

I think these are welcome changes, and my changes are a bit nitpicky. I'm fine with the Format function, even though I think that just adding a Detailer interface would be better. I will be making that as a suggestion to the Go team.

💖 💪 🙅 🚩
dean
dean

Posted on September 3, 2018

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Go 2 Draft: Error Values
go Go 2 Draft: Error Values

September 3, 2018