Better Golang Error Handling
Tags: go, programming, design, errors
Published: 2022-08-15 04:11:12

_**TL;DR Skip to the end to see the code**_

## Motivation

[I recently reviewed](https://dev.to/wspowell/golang-error-everything-you-know-is-wrong-o7j) various reasons why the built-in golang `error` is not great. In fact, if you [Google "golang error handling"](https://www.google.com/search?q=golang+error+handling) you will find many other blogs with various takes on how to navigate errors in golang. Even though the topic has seen lots of conversation, there really has not been much progress.

So I will throw out yet another blog post on golang error handling.

## Start Fresh

First thing I will propose, though controversial, is to throw out everything there is about golang errors. Do not use `error`. Do not use the `"errors"` package. Just throw it all away (it was probably just giving you anxiety anyway). `error` was more like a guideline anyway.

Now that we have no error handling we are open to a new world of possibilities. What does an error look like? What data should be contained in an error? Once created, how do we handle an error? Can we extend errors now? In this blog, I propose an error of the simplest possible terms so that it may be extended only as needed.

## The `Error` Type

`type Error[T ~string] string`

Yep. Let us just start with a new type called `Error`. Note that we could also just reuse `error` since it is not a keyword, but that gets slightly messy. This new `Error` is simply just a `string`. `string` is a great starting point because errors messages are usually strings that are human readable.

Right away, we essentially have everything we need. An error could be `"whoops"` and no error would be `""` (empty string). To make the usage around this a bit nicer to read, we can attach `IsSome() bool` and `IsNone() bool`. These two helper functions would just check if the value is `""`.

For example:
```
// Create an error
err := errors.New[string]("whoops")

// Handle the error.
if err.IsSome() {
    // Do something useful with the error.
}
```

While this sufficiently covers creating errors and error handling, there is more that we can do. In fact, while these helper functions may remain on a possible real implementation, I would propose never using them. The reason? There is actually an even better approach that utilizes that generic `T` on the `Error` type.

## Error Enums

One major issue with golang is the lack of enums. However, there are ways to make enums that linters will recognize. This usually follows in the form of:
```
const EnumType <some type>

const (
    EnumTypeA = EnumType(<some value>)
    EnumTypeB = EnumType(<some value>)
    // ... and so on
)
```

Taking some inspiration from rust, it would be amazing if we could have something similar to `match` that would force us to cover all enum cases. Fortunately, the next best thing is the [exhaustive linter](https://golangci-lint.run/usage/linters/#exhaustive). 

In the current version of golang `error`, the only way to create a sentinel error is to do `var ErrMyError = errors.New("whoops")`. But there is a problem with this: it is a variable, not constant. So this is not a real fake enum that linters recognize. It also does not make sense for these to be mutable globals.

The proposed new `Error` type solves this issue by accepting a `T ~string`. This means the type of the `Error` itself is different depending on `T`. `T` is also constrained to `~string` which means two things: `T` can be cast to `string`, `T` can be const. Note that even though `T` is not stored as a value in the `Error` type, we can still reference it because it is part of the type definition. This allows us to keep `Error` as a simple `string` while being able to cast it into `T`. Taking some more inspiration from rust, we can add a function to `Error` that does this:
```
func (self Error[T]) Into() T {
	return T(self)
}
```

While this all feel like over-complicated code since it is all just syntax sugar around `string`, it enables us to now have error handling like this:
```
switch err := StringError(0); err.Into() {
default:
    fmt.Println(err)
case errors.ErrNone:
    fmt.Println("no error")
}
```

For a simple `Error[string]`, `default` catches an error if it exists and `case errors.ErrNone` catches the "no error" case. However, if we have an error enum then that enum type is the `T` and `Into() T` returns the error of that type. This looks like:
```
_, err = EnumError(3)
switch err.Into() {
case ErrMyErrorOne:
    fmt.Println(err)
case ErrMyErrorTwo:
    fmt.Println(err)
case ErrMyErrorThree:
    fmt.Println(err)
case errors.ErrNone:
    fmt.Println("no error")
}
```
Notice there is a case for each enum that was defined and there is no `default`. Technically, to be thorough `default` should be included but if you have to return an error of a certain type then that should not happen, but it could.

What about missing cases? If there is a missing case, the exhaustive linter will point it out and let you know that you did not include everything.
![missing cases in switch of type SampleError: ErrMyErrorOne, ErrMyErrorThree, ErrMyErrorTwo (exhaustive)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zoaelbpc5pepq8qwvka1.png)

## Extending `Error`

The golang `error` will let you extend it and do whatever you want and any viable replacement should allow the same functionality. It very well may be that a `string` is not sufficient for some use cases. In that case, we still need a way of extending `Error` without having to change the pattern.

Let us assume that we desire a unique error code for tracking/debugging purposes that is assigned to each `Error`. We can extend `Error` by creating our new `type MyError[T ~string] struct` and embedding `Error` in it. `MyError` now has access to the same functionality that `Error` does while also having the benefit of storing an extra value for an error ID.

```
type MyError[T ~string] struct {
    errors.Error[T]

    // Other data defined here.
    errId int
}

func New[T ~string](err T, errId int) MyError[T] {
    return MyError[T]{
        Error: errors.New(err),
        errId: errId,
    }
}
```

The usage is exactly the same except `MyError[T]` is returned from a function instead of `Error[T]`.

## Advantages/Benefits

So why go through all this effort? Maybe you are satisfied with golang `error` and need more convincing? Aside from what has already been stated in the sections above, there are more benefits gained from this proposed approach.

### Performance
The proposed `Error` is actually more performant than `error`. This is because `error` is an interface which means that literally every error ever created requires a memory allocation. In straight up benchmark comparisons, this is not all that much, but all of those allocations add up.

Here is my non-official benchmark comparison between creating a new golang `error` and an `Error`.
```
error
24.96 ns/op	      16 B/op	       1 allocs/op
Error
5.557 ns/op	       0 B/op	       0 allocs/op
```
A ~4.5x improvement is not too shabby even though we are talking about nanoseconds here.

### Self Documenting Code
Documentation is always a struggle. The moment code changes, the documentation must be updated. However, using enums as types in `Error` allows us to clearly see what errors a function returns. I have used some comments like this before but it just does not feel great:
```
// Foo does stuff
//
// Errors:
//   * ErrFooFailure
func Foo() error { ... }
```

Instead, we can now have something like this:
```
type SampleError string

const (
    ErrMyErrorOne   = SampleError("error one")
    ErrMyErrorTwo   = SampleError("error two")
    ErrMyErrorThree = SampleError("error three")
)

func EnumError(which int) (int, errors.Error[SampleError]) {
    switch which {
    case 1:
        return 1, errors.New(ErrMyErrorOne)
    case 2:
        return 2, errors.New(ErrMyErrorTwo)
    case 3:
        return 3, errors.New(ErrMyErrorThree)
    default:
        return 0, errors.None[SampleError]()
    }
}
```

This is clear, ties in with the compiler, and is easy to understand. The code simply documents itself.

### "Accept interfaces, return structs"
This is a general rule in golang code, but `error` completely breaks this since it is an interface. Once returned, there is no way to operate on the error except through helper functions like `errors.As()`. Being required to call `Unwrap()`, `errors.As()`, etc means there is uncertainty, which usually leads to bugs. The uncertainty I am referring to is all of the issues mentioned in my previous post, such as error formatting. Since `Error` is not an interface, we can operate on it with certainty.

### No Wrapping
This touches on another small performance benefit. When `"errors"` was abandoned for this proposal, we also dumped all of that error wrapping insanity. `error` is used as a linked list. Since `Error` is intended to be immutable and "one shot", there is no chain of errors that must be traversed to check if we got some type of `error`. And with the recent release of `go1.19`, if you happened to have a large set of errors then the proposed `switch` error handling pattern would benefit from a [switch jump table](https://tip.golang.org/doc/go1.19#compiler).

## Next Steps

This is proposal is something I am still flushing out, but I feel like it shows real promise. In fact, in just writing this I changed a few things, but the core concept remains. I will keep iterating and experimenting my personal projects, though it may take some some convincing to use an iteration of this in actual projects.

## TL;DR Just Show Me The Code!
`error.go`
```
package errors

import (
	"fmt"
)

const ErrNone = ""

func None[T ~string]() Error[T] {
	return Error[T]("")
}

func New[T ~string](format T, values ...any) Error[T] {
	return newError(format, values...)
}

type Error[T ~string] string

func newError[T ~string](format T, values ...any) Error[T] {
	var err string
	if len(values) != 0 {
		// Do not call fmt.Sprintf() if not necessary.
		// Major performance improvement.
		// Only necessary if there are any values.
		err = fmt.Sprintf(string(format), values...)
	} else {
		err = string(format)
	}

	return Error[T](err)
}

func (self Error[T]) IsNone() bool {
	return self == ""
}

func (self Error[T]) IsSome() bool {
	return !self.IsNone()
}

func (self Error[T]) Into() T {
	return T(self)
}
```

`error_example_test.go`
```
package errors_test

import (
	"fmt"

	"experimentation/errors"
)

func StringError(which int) errors.Error[string] {
	switch which {
	case 1:
		return errors.New("error: %s", "whoops")
	default:
		return errors.None[string]()
	}
}

func ExampleStringError() {
	switch err := StringError(0); err.Into() {
	default:
		fmt.Println(err)
	case errors.ErrNone:
		fmt.Println("no error")
	}

	switch err := StringError(1); err.Into() {
	default:
		fmt.Println(err)
	case errors.ErrNone:
		fmt.Println("no error")
	}

	// Output:
	// no error
	// error: whoops
}

type SampleError string

const (
	ErrMyErrorOne   = SampleError("error one")
	ErrMyErrorTwo   = SampleError("error two")
	ErrMyErrorThree = SampleError("error three")
)

func EnumError(which int) (int, errors.Error[SampleError]) {
	switch which {
	case 1:
		return 1, errors.New(ErrMyErrorOne)
	case 2:
		return 2, errors.New(ErrMyErrorTwo)
	case 3:
		return 3, errors.New(ErrMyErrorThree)
	default:
		return 0, errors.None[SampleError]()
	}
}

func ExampleEnumError() {
	_, err := EnumError(0)
	switch err.Into() {
	default:
		fmt.Println(err)
	case errors.ErrNone:
		fmt.Println("no error")
	}

	_, err = EnumError(3)
	switch err.Into() {
	case ErrMyErrorOne:
		fmt.Println(err)
	case ErrMyErrorTwo:
		fmt.Println(err)
	case ErrMyErrorThree:
		fmt.Println(err)
	case errors.ErrNone:
		fmt.Println("no error")
	}

	// Output:
	// no error
	// error three
}

type MyError[T ~string] struct {
	errors.Error[T]

	// Other data defined here.
	errId int
}

func New[T ~string](err T, errId int) MyError[T] {
	return MyError[T]{
		Error: errors.New(err),
		errId: errId,
	}
}

func (self MyError[T]) String() string {
	return fmt.Sprintf("error: %s, id: %d", self.Error, self.errId)
}

func MyErrorFn() MyError[string] {
	return New("whoops", 123)
}

func ExampleMyError() {
	switch err := MyErrorFn(); err.Into() {
	default:
		fmt.Println(err)
	case errors.ErrNone:
		fmt.Println("no error")
	}

	// Output:
	// error: whoops, id: 123
}
```