Typed API errors with errors.Is and errors.As

Go
package main

import (
	"errors"
	"fmt"
	"net/http"
)

// APIError carries an HTTP status so handlers can map errors to responses.
type APIError struct {
	Status int
	Code   string
	Err    error
}

func (e *APIError) Error() string { return fmt.Sprintf("%s: %v", e.Code, e.Err) }

func (e *APIError) Unwrap() error { return e.Err } // errors.Is/As reach the cause
var ErrNotFound = errors.New("not found")

func findUser(id int) error {
	return &APIError{Status: http.StatusNotFound, Code: "user_not_found", Err: ErrNotFound}
}

func respond(err error) string {
	var apiErr *APIError
	if errors.As(err, &apiErr) {
		return fmt.Sprintf("http %d (%s)", apiErr.Status, apiErr.Code)
	}
	if errors.Is(err, ErrNotFound) {
		return "http 404 (not found)"
	}
	return "http 500 (internal error)"
}

func main() {
	err := findUser(7)
	fmt.Println(err, respond(err), errors.Is(err, ErrNotFound))
}