How do you create a custom error type in Go?

5 minintermediategocustom-errorserror-types

Quick Answer

Define a type (usually a struct) with an Error() string method, satisfying the built-in error interface: type ValidationError struct { Field, Msg string }; func (e *ValidationError) Error() string { return e.Field + ": " + e.Msg }. Return it as a normal error (return &ValidationError{...}), and callers use errors.As to check for it and extract its fields when they need the extra structured data it carries. A custom error type is worth the extra code specifically when callers need programmatic access to details beyond a plain message string — like a field name, an HTTP status code, or a retry-after duration — which a plain sentinel error (just an opaque value) can't provide.

Detailed Answer

A custom error type is just a normal Go type with one required method. The value comes from what extra data it can carry.

Defining one

type ValidationError struct {
    Field string
    Msg   string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Msg)
}

Using a pointer receiver (*ValidationError) is the common convention, so errors.As and comparisons behave consistently, and so the error can be wrapped/unwrapped cleanly.

Returning and consuming it

func validateAge(age int) error {
    if age < 0 {
        return &ValidationError{Field: "age", Msg: "must be non-negative"}
    }
    return nil
}

err := validateAge(-5)
var valErr *ValidationError
if errors.As(err, &valErr) {
    fmt.Println("problem field:", valErr.Field)  // "age" — real structured data, not just a string
    // could also drive different HTTP status codes, retry logic, etc. based on valErr's fields
}

Adding an Unwrap method for chaining

type QueryError struct {
    Query string
    Err   error
}

func (e *QueryError) Error() string {
    return fmt.Sprintf("query %q failed: %v", e.Query, e.Err)
}

func (e *QueryError) Unwrap() error {
    return e.Err   // lets errors.Is/errors.As see through to the wrapped cause
}

Implementing Unwrap() error on a custom type makes it participate in errors.Is/errors.As chains, exactly like fmt.Errorf("%w", ...) does automatically.

When it's worth the extra code

If every caller only ever needs a message and an identity check, a sentinel error is simpler and sufficient. Reach for a custom type once callers need to branch on something structured. That could be an HTTP status code, a field name to highlight in a UI, or a duration to wait before retrying.