errkind

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 15, 2026 License: MIT Imports: 6 Imported by: 1

README

errkind

English | 简体中文

errkind is a business error library that works with Go's standard error handling, providing stable error identities, structured diagnostics, and optional logging and protocol integrations.

Use a Kind to identify what failed, messages, attributes, and causes to explain this occurrence, and boundary adapters to decide what clients see. Business code does not need HTTP status codes or a custom struct for every error type.

Core Features

  • Stable business identity — define Define(10001, "user_not_found") once and match with errors.Is(err, UserNotFound), without relying on error text.
  • Preserved diagnostics — create instances with New(msg) / Wrap(cause, msg), attach data with With(key, value), and keep using %w, errors.As, and errors.Join.
  • One-line logging — slog, zap, zerolog, and logrus emit the same structured fields, including cause trees.
  • Explicit public responses — HTTP/gRPC hide internal diagnostics by default. Choose status, message, and fields at the return boundary, without a mandatory central mapping table.
  • Use only what you need — no third-party dependencies in the core. Start with the default registry; add separate registries and stack capture when needed.

Installation

Requires Go 1.25.0+.

go get github.com/im-wmkong/errkind

http and slog are included with the core. Install third-party integrations as needed, for example gRPC:

go get github.com/im-wmkong/errkind/grpc

For other integrations, replace the final grpc path segment with zap, zerolog, logrus, or otel.

This guide describes the unreleased v0.2.0 API. These commands cannot yet install the documented version and new integration paths; to try them now, follow the local development instructions under Contributing.

Capability Map

All import paths start with github.com/im-wmkong/errkind.

Package Responsibility Key APIs
errkind Error identity and diagnostics Define, New, Wrap, With, KindOf / CodeOf / NameOf / MessageOf / AttrsOf, NewRegistry, CaptureStack / StackOf
/slog, /zap, /zerolog, /logrus Structured logging One-line Err / Fields; preserve code / name / message / attrs / stack / causes
/http Public HTTP responses Write, ResponseOf, Status / Message / Identity / Field, optional Responder
/grpc Thin gRPC conversion ToStatus, FromStatus, Code / Message / Identity / Field
/otel Trace diagnostics RecordError, Attributes, per-call Prefix
cmd/errkind, cmd/errkindlint Error-code tooling Markdown/JSON catalogues and definition checks

Quick Start

Put the next three snippets into the same main.go, in order, then run go run ..

1. Define an error kind
package main

import (
    "database/sql"
    "errors"
    "fmt"
    "log/slog"
    "os"

    "github.com/im-wmkong/errkind"
    slogerr "github.com/im-wmkong/errkind/slog"
)

var UserNotFound = errkind.Define(10001, "user_not_found")

A Kind is a stable identity, usually defined once at package scope. Duplicate codes or names within a registry panic. It carries no default message or protocol status.

2. Create an instance and keep its cause
func findUser(id int64) error {
    return UserNotFound.Wrap(sql.ErrNoRows, "lookup user", errkind.With("uid", id))
}

Use Wrap(cause, msg, opts...) when there is an underlying error, or New(msg, opts...) when there is not. The message describes this operation and attributes hold its data. Wrap(nil, msg) returns nil.

3. Match and log the error
func main() {
    err := findUser(42)
    fmt.Println(errors.Is(err, UserNotFound))
    fmt.Println(errors.Is(err, sql.ErrNoRows))

    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    logger.Error("request failed", slogerr.Err(err))
}

Both checks print true: the business identity and underlying cause are preserved. The log's err field is:

{
  "code": 10001,
  "name": "user_not_found",
  "message": "lookup user",
  "attrs": {"uid": 42},
  "causes": [{"message": "sql: no rows in result set"}]
}

Callers only need the shared UserNotFound definition to handle this failure, not knowledge of the database. Logs contain internal diagnostics and should not be used directly as client responses.

Patterns at a Glance

Use the following snippets independently in the appropriate functions or packages, reusing UserNotFound above. Do not repeatedly execute Kind definitions.

Create and inspect structured errors
err := UserNotFound.New("active user is missing",
    errkind.With("uid", 42),
    errkind.With("operation", "profile"),
)

code, ok := errkind.CodeOf(err)
fmt.Println(code, ok)
fmt.Println(errkind.MessageOf(err))
fmt.Println(errkind.AttrsOf(err))

This prints 10001 true, active user is missing, and [{uid 42} {operation profile}]. msg is a required argument; pass "" when no additional message is needed, or use fmt.Sprintf to format it. Repeated attribute keys keep the last value and the first insertion position.

Connect an existing logger

Your application initializes the logger and chooses the level. Log once at the boundary that handles the error, rather than repeatedly in every layer.

Framework Adapter import Call
slog slogerr "github.com/im-wmkong/errkind/slog" logger.Error("failed", slogerr.Err(err))
zap zaperr "github.com/im-wmkong/errkind/zap" logger.Error("failed", zaperr.Err(err))
zerolog zeroerr "github.com/im-wmkong/errkind/zerolog" logger.Error().Func(zeroerr.Err(err)).Msg("failed")
logrus logruserr "github.com/im-wmkong/errkind/logrus" logger.WithFields(logruserr.Fields(err)).Error("failed")

All four integrations use the same nested diagnostic object. Choose JSON output for field-based queries: NewJSONHandler for slog or JSONFormatter for logrus. Ordinary framework calls such as zap.Error(err) do not automatically use errkind's structured tree.

Choose public content at the HTTP boundary

Place this helper in the handler's package, reusing UserNotFound. The handler should log a returned write error rather than attempt a second response.

import (
    "net/http"
    "github.com/im-wmkong/errkind"
    httperr "github.com/im-wmkong/errkind/http"
)

func writeUserError(w http.ResponseWriter, err error, requestID string) error {
    if errkind.KindOf(err) == UserNotFound {
        return httperr.Write(w, err,
            httperr.Status(http.StatusNotFound),
            httperr.Message("User not found"),
            httperr.Identity(),
            httperr.Field("request_id", requestID),
        )
    }
    return httperr.Write(w, err)
}

For a missing-user error with requestID="req-42", it returns HTTP 404:

{"code":10001,"name":"user_not_found","message":"User not found","fields":{"request_id":"req-42"}}

The internal message, uid, and database cause are not copied. Other non-nil errors default to HTTP 500 and {"message":"Internal Server Error"}. Status does not set the public message automatically; specify each separately.

For an existing web framework or response envelope, use ResponseOf(err, opts...) and pass its Status and Body to the framework. If endpoints genuinely share presentation rules, use Responder(func(context.Context, error) []Option) without a mandatory mapping system.

Convert gRPC statuses and match on the client

Import grpcerr "github.com/im-wmkong/errkind/grpc" and standard gRPC package google.golang.org/grpc/codes. This example performs a local conversion round trip; a real client first reads the incoming status with status.FromError(rpcErr).

st := grpcerr.ToStatus(UserNotFound.New("lookup user"),
    grpcerr.Code(codes.NotFound),
    grpcerr.Message("User not found"),
    grpcerr.Identity(),
)

remote := grpcerr.FromStatus(st, errkind.DefaultRegistry())
fmt.Println(errors.Is(remote, UserNotFound))
fmt.Println(errkind.MessageOf(remote))

It prints true and User not found. The server returns st.Err(). On receipt, both code and name must match in the supplied registry to bind a local Kind. Pass a nil registry to read remote information without binding identity.

The integration does not install interceptors or manage streams. See examples/grpc for a real RPC call.

Separate registries and stack capture
registry := errkind.NewRegistry(errkind.CaptureStack())
DatabaseFailed := registry.Define(20001, "database_failed")
err := DatabaseFailed.New("connect database")
fmt.Printf("%+v\n", err)

In application code, create the registry and Kind once at package scope. The default registry does not capture stacks. Create separate registries only when you need definition isolation or stack capture. Identical code/name pairs in different registries are still different Kinds.

Return multiple failures together
BatchFailed := errkind.Define(30001, "batch_failed")
joined := errors.Join(UserNotFound.New("lookup user"), errors.New("cache unavailable"))

fmt.Println(errors.Is(joined, UserNotFound))
fmt.Println(errkind.KindOf(joined) == nil)

err := BatchFailed.Wrap(joined, "load profile")
fmt.Println(errkind.KindOf(err) == BatchFailed)

All three lines print true. errors.Is asks whether the tree contains a kind; KindOf asks for the primary failure. A bare multi-cause Join has no unique primary error. An outer BatchFailed explicitly classifies the operation while logs retain every cause branch.

Record on an existing span

Import otelerr "github.com/im-wmkong/errkind/otel"; the application creates span:

otelerr.RecordError(span, err, otelerr.Prefix("biz.err."))

This adds a standard error event, Error status, and primary-error attributes. The full cause tree is stored in biz.err.diagnostic. Use otelerr.Attributes(err) if you only need attributes. The default prefix is err.; include the trailing dot in custom prefixes.

Examples and Details

Run these commands from the repository root:

Topic Runnable example Command
Creation, matching, and logging examples/basic go run ./examples/basic
HTTP handlers and safe responses examples/http go run ./examples/http
gRPC server and client examples/grpc go -C examples/grpc run .

The HTTP example listens on 127.0.0.1:8080: /user?id=42, /user?id=0, /user?id=999, and /user?id=500 return 200, 400, 404, and 500. It uses Chinese public messages. The gRPC example uses an in-memory connection and needs no external service.

Error inspection, attributes, and logging semantics
  • KindOf / CodeOf / NameOf / MessageOf / AttrsOf select the outermost business instance along a single chain. If multiple non-nil branches appear first, they do not arbitrarily select one.
  • CodeOf / NameOf use a bool for presence; business code zero is valid. An unbound remote error can have code/name while KindOf is nil. A bare Kind is a matching target, not a diagnostic instance; create instances with New / Wrap.
  • MessageOf reads only the primary error's message, falling back to Error() without a business instance and returning an empty string for nil. Use Error() for full text or a logging integration for complete diagnostics.
  • AttrsOf and Details.Attrs return shallow slice copies, not deep copies of maps, slices, or pointers stored as values. Copy before passing values when you need a snapshot, and do not mutate shared values concurrently.
  • AllAttrs is an explicitly lossy flat view: depth-first, left-to-right, keeping the first value for each key. Default logging does not merge sibling attributes.
  • Log objects contain code / name / message / attrs / stack / causes / truncated. Empty fields are omitted, zero business codes are kept, and nil is null. Ordinary Go errors retain their full text in message.
  • Custom log keys use slogerr.Value, zaperr.Object, zeroerr.Field, or logruserr.FieldsWithPrefix. The latter accepts an object key, not an expanded dot-key prefix.
  • An unencodable attribute or panicking encoder degrades only that value to <unencodable type>. Logs do not redact data automatically; do not store passwords or tokens in attributes.
  • Stack settings are fixed at Registry construction. Non-empty cause stacks suppress recapture. Tracer.StackTrace() reads this node only; StackOf finds the first non-empty stack. Logs attach stacks to their actual owners.
Protocol defaults, precedence, and failure handling
  • HTTP Identity publishes the primary code/name; Field adds to a separate fields object without automatically copying internal attrs. Nil errors write nothing.
  • HTTP statuses are limited to 400–599. Invalid status or public-field encoding failure makes Write send a safe 500 and return a construction error. Write failures are returned too. ResponseOf returns a safe response plus a non-nil error on construction failure, or (nil, nil) for nil input.
  • Responder.Write(w, r, err, opts...) applies safe defaults, the callback, then per-call options. The last assignment to a field wins. Nil errors skip the callback.
  • gRPC ordinary/business errors default to Internal and a generic message. Nil input returns nil; FromStatus also returns nil for nil/OK.
  • Without outer business reclassification or output options, native/remote gRPC statuses preserve code/message/details; their creator owns public-content safety. Context cancellation/deadline on an ordinary single chain preserves standard status codes.
  • Any gRPC output option rebuilds from safe defaults, without inheriting old details. Message alone does not preserve NotFound; also pass Code if needed. An outer business instance wrapping status/context does not automatically inherit the inner status either.
  • gRPC Field accepts strings only; only Identity sends business identity. Invalid codes, invalid UTF-8, or reserved _errkind.* key writes fall back to generic Internal. Identity uses the fixed errkind/v1 ErrorInfo marker; unrecognized, duplicate, or malformed identity is not bound, while the original status is preserved.
  • Remote binding represents the caller's choice of service contract, not authentication or authorization. Clients should define or import matching Kinds first; different services can use separate registries.
  • OTel records only on non-nil recording spans; nil errors are no-ops. Applications log final HTTP/gRPC status separately. OTel preserves integers outside int64 range as JSON text.
Error-code checks, documentation generation, and migration

Install the tools from the errkind source root, then run subsequent commands in your business project. Ensure Go's binary installation directory is on PATH:

go install ./cmd/errkind ./cmd/errkindlint
errkindlint ./...
errkind doc -format=md .
errkind doc -format=json -o=errors.json .

Lint checks duplicate codes/names, empty names, and scan errors, exiting non-zero on failure. Doc produces Code, Name, and Source, not a replacement for lint. -o overwrites its destination, but scan or argument-validation failures do not write it.

The tools recognize two literal arguments to errkind.Define, including import aliases. They do not resolve constants, Registry methods, dot imports, or function aliases, execute init, or filter by build tags. Scan independent applications separately; custom registries still rely on runtime registration checks.

When migrating from v0.1.x:

  • Replace DefaultMessage and core Message / Messagef options with New(msg, opts...) / Wrap(cause, msg, opts...). HTTP/gRPC public Message options remain.
  • Replace Kind.Is(err) with errors.Is(err, Kind) and global stack switches with NewRegistry(CaptureStack()).
  • Replace ext/http, ext/slog, and integration/<name> with top-level packages. Replace old decorators and gRPC interceptors with explicit boundary conversion.
  • Move log queries from flat cause / dot-keys to nested causes / err. Also check primary-error selection, remote binding, and implicit JSON behavior.

See CHANGELOG for the full history.

Known Limitations

  • For plain text context, use fmt.Errorf("operation: %w", err); not every layer needs a Kind. The library does not automatically retry or manage request lifecycles.
  • Attributes are shallow copies. Logs contain internal information and do not redact automatically. Core errors have no implicit JSON; use logging or protocol APIs.
  • Multi-cause errors do not automatically get a primary identity. Core extraction and stack searches inspect at most 256 nodes; log trees are limited to 256 nodes and 64 levels, with truncated:true when children are omitted.
  • API and package paths have breaking changes from v0.1.x. This candidate is unpublished and provides no old-path forwarding packages.

Contributing

Issues and Pull Requests are welcome. Before submitting, run these from the repository root:

bash scripts/test.sh -race -cover -count=2
go run ./cmd/errkindlint -exclude=examples/ .
python3 scripts/verify-release.py

The test script covers all seven modules; root go test ./... does not cross module boundaries. Consumer verification checks external usage and CLI installation with isolated caches, downloading dependencies but publishing nothing. Run benchmarks with go test -run='^$' -bench=. -benchmem . ./internal/diagnostic ./slog.

Keep README.md and README_CN.md in sync when changing either one.

Local development: use unpublished source in another project

To run the repository example, simply execute go run ./examples/basic from this checkout's root.

For testing in another project, replace ERRKIND_DIR with this checkout's absolute path. Start in a new empty directory; skip go mod init in an existing Go project.

ERRKIND_DIR=/absolute/path/to/errkind
go mod init example.com/errkind-demo
go mod edit -require=github.com/im-wmkong/errkind@v0.2.0
go mod edit "-replace=github.com/im-wmkong/errkind=$ERRKIND_DIR"

http and slog come with the core. Third-party integrations are separate modules; point each one you use at its source, for example gRPC:

INTEGRATION=grpc
go mod edit "-require=github.com/im-wmkong/errkind/$INTEGRATION@v0.2.0"
go mod edit "-replace=github.com/im-wmkong/errkind/$INTEGRATION=$ERRKIND_DIR/$INTEGRATION"

Replace grpc with zap, zerolog, logrus, or otel as needed. After adding code and imports, run go mod tidy and keep the core module's local replacement. The candidate version is paired with local source, not a claim that it has been published.

License

MIT License.

Documentation

Overview

Package errkind separates error identity from instance diagnostics and public responses.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func MessageOf

func MessageOf(err error) string

func NameOf

func NameOf(err error) (string, bool)

Types

type Attr

type Attr struct {
	Key string
	Val any
}

func AllAttrs

func AllAttrs(err error) []Attr

AllAttrs is a lossy, depth-first flat view; the first value for each key wins.

func AttrsOf

func AttrsOf(err error) []Attr

type Code

type Code uint32

func CodeOf

func CodeOf(err error) (Code, bool)

type Details added in v0.2.0

type Details interface {
	error
	BusinessCode() (Code, bool)
	Name() string
	Message() string
	Attrs() []Attr
}

Details exposes instance diagnostics; Attrs returns a shallow copy owned by the caller.

type Frame

type Frame struct {
	Function string
	File     string
	Line     int
}

Frame 是调用栈一帧。

func StackOf added in v0.2.0

func StackOf(err error) []Frame

StackOf 返回错误树中第一个非空调用栈。

func (Frame) String

func (f Frame) String() string

String 输出 "package.Func\n\tfile:line"。

type Kind

type Kind struct {
	// contains filtered or unexported fields
}

func Define

func Define(code Code, name string) *Kind

func KindOf

func KindOf(err error) *Kind

KindOf returns the primary instance's bound identity, never an arbitrary sibling's.

func Kinds

func Kinds() []*Kind

Kinds 返回默认注册中心的所有 Kind。

func LookupCode

func LookupCode(c Code) *Kind

LookupCode 默认注册中心查询。

func LookupName

func LookupName(n string) *Kind

LookupName 默认注册中心查询。

func (*Kind) Code

func (k *Kind) Code() Code

func (*Kind) Error added in v0.2.0

func (k *Kind) Error() string

func (*Kind) Name

func (k *Kind) Name() string

func (*Kind) New

func (k *Kind) New(msg string, opts ...Option) error

func (*Kind) Wrap

func (k *Kind) Wrap(cause error, msg string, opts ...Option) error

type Option

type Option func(*kerr)

func With

func With(key string, val any) Option

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry 持有一组 Kind, 保证 (code, name) 在自身范围内唯一。

通常使用包级 Define / Kinds / LookupCode / LookupName 即可; 测试或多租户场景可以 NewRegistry() 创建独立注册中心。

func DefaultRegistry added in v0.2.0

func DefaultRegistry() *Registry

func NewRegistry

func NewRegistry(opts ...RegistryOption) *Registry

func (*Registry) Define

func (r *Registry) Define(code Code, name string) *Kind

Define 注册并返回一个新的 Kind; 重复 code/name 立即 panic。

func (*Registry) Kinds

func (r *Registry) Kinds() []*Kind

Kinds 返回所有已注册 Kind, 按注册顺序; 用于生成错误码文档。

func (*Registry) LookupCode

func (r *Registry) LookupCode(c Code) *Kind

LookupCode 按 code 查找; 不存在返回 nil。

func (*Registry) LookupName

func (r *Registry) LookupName(n string) *Kind

LookupName 按 name 查找; 不存在返回 nil。

type RegistryOption added in v0.2.0

type RegistryOption func(*registryConfig)

func CaptureStack added in v0.2.0

func CaptureStack() RegistryOption

type Tracer

type Tracer interface {
	StackTrace() []Frame
}

Tracer 表示能提供调用栈的错误。

Directories

Path Synopsis
cmd
errkind command
Command errkind 提供错误码工程相关子命令; 当前实现 doc 子命令用于生成错误码文档。
Command errkind 提供错误码工程相关子命令; 当前实现 doc 子命令用于生成错误码文档。
errkindlint command
Command errkindlint 静态扫描 errkind.Define 调用, 检查 (code, name) 冲突。
Command errkindlint 静态扫描 errkind.Define 调用, 检查 (code, name) 冲突。
examples
basic command
http command
Package http builds public responses independently of error diagnostics.
Package http builds public responses independently of error diagnostics.
integration
grpc module
internal
docgen
Package docgen 把扫描得到的 errkind.Define 列表渲染成错误码文档。
Package docgen 把扫描得到的 errkind.Define 列表渲染成错误码文档。
lint
Package lint 基于 internal/scan 的扫描结果, 检测 (code, name) 冲突与显然错误。
Package lint 基于 internal/scan 的扫描结果, 检测 (code, name) 冲突与显然错误。
scan
Package scan 提供静态扫描能力, 在源码层面找出对 errkind.Define 的调用, 用于 errkindlint (冲突检测) 和 errkind doc (文档生成) 共享同一份解析逻辑。
Package scan 提供静态扫描能力, 在源码层面找出对 errkind.Define 的调用, 用于 errkindlint (冲突检测) 和 errkind doc (文档生成) 共享同一份解析逻辑。
Package slog provides explicit structured error logging for log/slog.
Package slog provides explicit structured error logging for log/slog.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL