Go by Example

Custom Errors

Implementing the error interface with a struct to carry structured context callers can extract.

Any type that implements Error() string satisfies the error interface. A custom error struct lets you attach structured context - HTTP status codes, resource IDs, error codes - that callers extract with errors.As without parsing strings.

Define a struct with the fields you need, implement Error() string, and return the concrete type as an error interface from public APIs.

package main
 
import (
    "errors"
    "fmt"
)
 
type RequestError struct {
    StatusCode int
    Method     string
    URL        string
}
 
func (e *RequestError) Error() string {
    return fmt.Sprintf("%s %s: status %d", e.Method, e.URL, e.StatusCode)
}
 
func fetchData(url string) error {
    // simulated HTTP call returning 404
    return &RequestError{
        StatusCode: 404,
        Method:     "GET",
        URL:        url,
    }
}
 
func main() {
    err := fetchData("/api/users/99")
    if err != nil {
        fmt.Println(err)
 
        var re *RequestError
        if errors.As(err, &re) {
            fmt.Printf("HTTP %d on %s %s\n", re.StatusCode, re.Method, re.URL)
        }
    }
}

You can also implement Unwrap() error to participate in error chain inspection:

type DBError struct {
    Op  string
    Err error // underlying cause
}
 
func (e *DBError) Error() string  { return fmt.Sprintf("db %s: %v", e.Op, e.Err) }
func (e *DBError) Unwrap() error  { return e.Err }
 
var ErrConnFailed = errors.New("connection failed")
 
err := &DBError{Op: "query", Err: ErrConnFailed}
fmt.Println(errors.Is(err, ErrConnFailed)) // true - Unwrap lets Is walk the chain

In production

Return the concrete error type as an error interface from public functions - returning *RequestError directly leaks implementation details and breaks errors.As chains when callers use the interface. A custom error type lets callers extract structured context (HTTP status, error code, resource ID) without string parsing. Accept error in function signatures, not concrete types; code that receives an error is decoupled from the specific implementation. Implement Unwrap() error when your custom error wraps another error so errors.Is and errors.As can traverse the full chain.

Enjoyed this? Get more essays on software craft delivered to your inbox.

Subscribe free