Skip to main content

Union error type in Go

· 4 min read
at15
Software Engineer

Force returning a subset of error types from a function in Go.

Background

Go has a very simple error handling mechanism. A function in Go can return multiple values, the last value can be an error type. The error interface is simple:

type error interface {
Error() string
}

The easiest way to create a new error is using fmt.Errorf or errors.New. However, error created from string is pretty freeform and caller cannot extract structured information out of it reliably. A more structured way is to define a struct that implements the error interface. For example, we can define a UserError and ValidationError:

type UserError struct {
UserName string
Code int // assume this service has some registry of error code
Message string
}

func (e *UserError) Error() string {
return fmt.Sprintf("user: %s, code: %d, message: %s", e.UserName, e.Code, e.Message)
}

type ValidationError struct {
Message string
Field string
}

func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error on field: %s, message: %s", e.Field, e.Message)
}

To return one of the error types, we just write error in the function signature, e.g.

func doSomething(v string) error {
if v == "" {
return &ValidationError{
Message: "value is empty",
Field: "v",
}
}
if v == "admin" {
return &UserError{
UserName: "admin",
Code: 403,
Message: "user is not allowed",
}
}
return nil
}

Now there are two issues we need to address:

  • Let the caller know what type of error will be returned without looking at the implementation.
  • Force the implementation to return one of the error types and not using fmt.Errorf lazily.

In Rust, you can use Result<T, E> where E is a union of error types, e.g.

enum MyError {
UserError(UserError),
ValidationError(ValidationError),
}

In Go, there is no union (or pattern matching...), the closest is interface and type assertion.

Define a interface as union for error types

We can define a interface that has a speical marker. e.g. APIError

type APIError interface {
error
IsAPIError()
}

func (u *UserError) IsAPIError() {}
func (u *ValidationError) IsAPIError() {}

Now we can use APIError in the function signature, e.g.

func doSomething(v string) APIError {
if v == "" {
return &ValidationError{
Message: "value is empty",
Field: "v",
}
}
if v == "admin" {
return &UserError{
UserName: "admin",
Code: 403,
Message: "user is not allowed",
}
}
return nil
}

To inspect the error type after wrapping using fmt.Errorf, we can use errors.As.

func wrapError(v string) error {
err := doSomething(v)
if err == nil {
return nil
}
return fmt.Errorf("wrapped: %w", err)
}

var _ APIError = &UserError{}
var _ APIError = &ValidationError{}

func TestAPIError(t *testing.T) {
err := wrapError("admin")
var u *UserError
if errors.As(err, &u) {
t.Logf("user error: %s", u.Error())
}
err = wrapError("")
var v *ValidationError
if errors.As(err, &v) {
t.Logf("validation error: %s", v.Error())
}
}

This is not that straightfoward in the beginning, but makes the code easier to maintain in the long run. Especially if you are writing a library. The type system is more up to date than seprated documentation. Further more, for writing API server/client, these error types can be defined in schema and generated directly.

Why not using generic

Go's generic does allow you to define an interface that looks like a union, e.g.

type MyError interface {
UserError | ValidationError
}

However, you cannot return different types from a generic function, e.g.

func doSomething[T MyError](v string) T {
if v == "" {
return &ValidationError{
Message: "value is empty",
Field: "v",
}
}
if v == "admin" {
return &UserError{
UserName: "admin",
Code: 403,
Message: "user is not allowed",
}
}
return nil
}

Will lead to error:

cannot use &ValidationError{…} (value of type *ValidationError) as T value in return statement: cannot assign *ValidationError to UserError (in T)

The polymorphism is compile time, not runtime like Java's interface.

References

Stacktrace (not related to this post, but came across them ...)