Go 2 Draft: Error Values
Tags: go, errors, go2
Published: 2018-09-03 07:04:47

---
title: Go 2 Draft: Error Values
published: true
description: "Go 2 may be coming out within the next couple of years, let's take a look at the upcoming language changes!"
tags: Go, errors, go2
---

# Go 2 is being drafted.

If you haven't [heard](https://www.youtube.com/watch?v=6wIP3rO6On8), there will be a few major language changes coming to `Go 2`. The Go team has recently submitted a [draft](https://go.googlesource.com/proposal/+/master/design/go2draft.md) 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](https://dev.to/deanveloper/go-2-draft-error-handling-3loo), 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:

```go
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
```

`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
```go
var errne *os.ErrNotExist
errors.AsValue(&errne, err)
```

## 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 <path>", and no good way to get more detail.

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

```go
type Formatter interface {
    Format(p Printer) (next error)
}
```

`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:
```go
// 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
}
```

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
```
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!

```go
// 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
}
```

And now we can use this error (note, I am using syntax from the "error handling" Go 2 proposal. I have a [blog post](https://dev.to/deanveloper/go-2-draft-error-handling-3loo) on that one already)

```go

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")
}

```

# 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...
```go
func (e *MyErr) Format(p errors.Printer) (next error) {
    p.Print(/* short error string */)
    if p.Detail() {
        p.Print(/* details */)
    }
    return /* caused by */
}
```
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...
```go
// 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
}
```
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.


[draft]: https://go.googlesource.com/proposal/+/master/design/go2draft-error-values-overview.md