Documentation
¶
Overview ¶
Package errext is a small, dependency-free extension of Go's standard errors package. It adds stack-trace capture, contextual prefix wrapping, structured JSON / log/slog output, optional caller-supplied metadata, and panic-recovery helpers, while preserving standard errors.Is / errors.As / errors.Unwrap semantics.
Quick start ¶
err := errext.Errorf("failed to process %s: %w", item, cause)
if errors.Is(err, cause) {
// standard errors.Is sees through errext wrappers
}
// Add context without mutating the wrapped error:
wrapped := errext.WrapPrefix(err, "validation failed", 0)
// Attach structured metadata (validated as JSON at set time):
md := json.RawMessage(`{"request_id":"abc-123"}`)
wrapped.(*errext.TraceError).SetMetadata(&md)
Core type ¶
The package's canonical type is *TraceError. All constructors return values backed by *TraceError; the historical Error interface is retained as a deprecated alias for source compatibility.
A *TraceError carries:
- the wrapped cause (visible via Unwrap and Cause),
- a captured runtime stack (Stack, StackFrames),
- an optional prefix (Prefix),
- optional caller-supplied JSON metadata (Metadata, SetMetadata).
It implements error, fmt.Formatter, json.Marshaler, and slog.LogValuer.
Behavior changes from earlier versions ¶
- Nil in, nil out. NewError(nil), Wrap(nil, ...), and WrapPrefix(nil, ..., ...) all return nil.
- Wrapping never mutates the wrapped error. Each Wrap / WrapPrefix call produces a new *TraceError with a fresh stack capture.
- StackFrames(), Stack(), and Metadata() return copies; callers may freely mutate the returned slices.
- Default stack formatting no longer reads source files from disk. The opt-in StackFrame.SourceLine helper remains for explicit callers.
- errext.Is is a thin wrapper around errors.Is.
Structured output ¶
JSON marshaling produces a Record value:
{
"message": "ctx: root cause", // == Error()
"cause": "root cause", // deepest non-TraceError cause
"type": "*errors.errorString",
"prefix": "ctx",
"stack_frames": [...],
"stack": [...],
"metadata": {...}
}
slog.LogValuer emits the same fields as a group attribute, omitting the raw stack PCs to keep log lines compact.
Concurrency ¶
All methods on *TraceError are safe for concurrent use. The wrapped cause, prefix, and captured PCs are immutable after construction; metadata access is guarded by an internal RWMutex.
Security note ¶
Stack frames may include absolute file paths and function names, and metadata may contain caller-controlled data. Do not expose the full structured representation to untrusted clients without first considering whether the contents are safe to disclose.
Index ¶
- Constants
- Variables
- func Is(err, target error) booldeprecated
- type Errordeprecated
- type ErrorSetterdeprecated
- type Record
- type StackFrame
- type TraceError
- func (e *TraceError) Cause() error
- func (e *TraceError) Error() string
- func (e *TraceError) Format(s fmt.State, verb rune)
- func (e *TraceError) LogValue() slog.Value
- func (e *TraceError) MarshalJSON() ([]byte, error)
- func (e *TraceError) Metadata() *json.RawMessage
- func (e *TraceError) Prefix() string
- func (e *TraceError) Record() Record
- func (e *TraceError) RuntimeStack() []byte
- func (e *TraceError) SetMetadata(metadata *json.RawMessage) error
- func (e *TraceError) Stack() []uintptr
- func (e *TraceError) StackFrames() []StackFrame
- func (e *TraceError) Type() string
- func (e *TraceError) UnmarshalMetadata(target any) error
- func (e *TraceError) Unwrap() error
Constants ¶
const DefaultMaxStackDepth = 50
DefaultMaxStackDepth is the default cap on captured program counters per error. Negative or zero values are clamped to this default at capture time.
Variables ¶
var MaxStackDepth = DefaultMaxStackDepth
MaxStackDepth caps the number of program counters captured per error.
Deprecated: this is a process-wide mutable global with no synchronization. New code should rely on the default. The variable is retained only for source compatibility; invalid values (<= 0) are clamped to DefaultMaxStackDepth at capture time.
Functions ¶
Types ¶
type Error
deprecated
type Error interface {
error
Cause() error
StackFrames() []StackFrame
Stack() []uintptr
Prefix() string
Type() string
RuntimeStack() []byte
Metadata() *json.RawMessage
SetMetadata(*json.RawMessage) error
UnmarshalMetadata(target any) error
}
Error is the historical exported interface of this package.
Deprecated: new code should accept the standard error interface and use type assertions to *TraceError for enriched access. This interface is retained for source compatibility.
func Errorf ¶
Errorf creates a *TraceError from a formatted message. It is the canonical alias for the historical NewErrorf and is the form the README recommends. %w directives participate in errors.Is / errors.As.
func NewError ¶
NewError returns a *TraceError wrapping cause. It returns nil if cause is nil, following the Go convention that nil means no error.
func NewErrorf
deprecated
NewErrorf creates a *TraceError from a formatted message. The message is produced via fmt.Errorf, so %w directives in format participate in errors.Is / errors.As walks through the wrapper.
Deprecated: prefer Errorf. NewErrorf is retained for source compatibility.
func ParsePanic ¶
ParsePanic converts a panic stack-trace string into a *TraceError. The input is expected to start with "panic: <message>" followed by the "goroutine N [running]:" section emitted by the Go runtime. Frames are parsed without recording program counters; StackFrames() will return the parsed entries as-is.
ParsePanic never panics on malformed input; it returns a descriptive error instead. For newer code that has access to the recovered value and the raw debug.Stack() bytes, prefer FromPanic.
func Wrap ¶
Wrap returns a *TraceError around err with a fresh stack capture at the call site. It returns nil if err is nil. Unlike historical behavior, Wrap never returns the input pointer aliased; wrapping an existing *TraceError produces a new wrapper that Unwraps to it.
stackToSkip is added to the number of frames hidden from the captured stack; 0 starts the stack at the caller of Wrap.
func WrapPrefix ¶
WrapPrefix returns a new *TraceError that wraps err and prepends prefix to the contextual error message. The wrapped error is not mutated. Calling Error() on the result walks the wrapper chain, so wrapping a prefixed TraceError yields "outer: inner: base".
WrapPrefix returns nil if err is nil.
type ErrorSetter
deprecated
type ErrorSetter interface {
SetMetadata(*json.RawMessage) error
}
ErrorSetter is the historical mutation interface.
Deprecated: prefer calling SetMetadata directly on *TraceError. This interface is retained for source compatibility and is reduced to metadata mutation only; the prefix is no longer mutable after construction.
type Record ¶
type Record struct {
// Message is the full contextual error message, identical to Error().
Message string `json:"message,omitempty"`
// Cause is the deepest non-TraceError cause message.
Cause string `json:"cause,omitempty"`
// Type is a diagnostic Go type string (e.g. "*errors.errorString",
// "panic"). It is not stable enough for domain control flow.
Type string `json:"type,omitempty"`
// Prefix is this wrapper's own prefix; it does not include prefixes
// contributed by wrapped TraceErrors.
Prefix string `json:"prefix,omitempty"`
// StackFrames contains resolved frame data with no source-code lines.
StackFrames []StackFrame `json:"stack_frames,omitempty"`
// Stack contains the raw captured program counters.
Stack []uintptr `json:"stack,omitempty"`
// Metadata is caller-supplied raw JSON.
Metadata *json.RawMessage `json:"metadata,omitempty"`
}
Record is the stable structured representation of a TraceError used by MarshalJSON and LogValue. Field names are part of the package's public surface; new fields may be added but existing ones will not silently change meaning.
type StackFrame ¶
type StackFrame struct {
// File is the absolute path to the source file containing the frame.
File string `json:"file"`
// LineNumber is the 1-based line within File.
LineNumber int `json:"line_number"`
// Name is the function name with any package prefix stripped.
Name string `json:"name"`
// Package is the import path of the package that contains the
// function, including a trailing slash when applicable.
Package string `json:"package"`
// ProgramCounter is the raw runtime program counter for the frame.
// It may be zero for frames that were parsed from a text stack trace.
ProgramCounter uintptr `json:"program_counter"`
}
StackFrame describes a single resolved frame of a captured stack. The fields carry source-location metadata but never contain the source-code line itself; the latter is available on demand via SourceLine.
func NewStackFrame ¶
func NewStackFrame(pc uintptr) StackFrame
NewStackFrame builds a StackFrame from a program counter. Frames whose program counter does not resolve to a known function are returned with only ProgramCounter populated.
func (StackFrame) Func ¶
func (s StackFrame) Func() *runtime.Func
Func returns the runtime.Func describing the frame, or nil if the program counter does not resolve.
func (*StackFrame) SourceLine ¶
func (s *StackFrame) SourceLine() (string, error)
SourceLine returns the line of source code referenced by the frame. It reads the file from disk, so callers should treat the result as opt-in debug aid and avoid invoking it on hot paths or untrusted file paths.
func (StackFrame) String ¶
func (s StackFrame) String() string
String returns a one-frame description suitable for diagnostic stack dumps. The format is:
<package>/<name> \t<file>:<line> +0x<pc>
String never reads source files from disk. To attach the source line, call SourceLine explicitly and append it.
type TraceError ¶
type TraceError struct {
// contains filtered or unexported fields
}
TraceError is an enriched error value with a captured stack trace, an optional contextual prefix, optional structured metadata, and standard errors.Unwrap support.
A *TraceError is immutable apart from its metadata slot. All methods are safe for concurrent use; reads of mutable state take a read lock, and SetMetadata takes a write lock. Wrapping an existing *TraceError produces a new *TraceError without mutating the wrapped value.
The zero value is not usable; obtain a *TraceError via NewError, Wrap, WrapPrefix, NewErrorf, Errorf, ParsePanic, or FromPanic.
func FromPanic ¶
func FromPanic(value any, stack []byte) *TraceError
FromPanic constructs a *TraceError from a value recovered via recover().
The cause is fmt.Sprint(value) reported with Type "panic". If stack is non-nil it is preserved as the error's runtime stack output; otherwise a fresh runtime.Callers capture is taken at the call site. The expected usage is:
defer func() {
if r := recover(); r != nil {
err = errext.FromPanic(r, debug.Stack())
}
}()
func (*TraceError) Cause ¶
func (e *TraceError) Cause() error
Cause returns the deepest non-TraceError cause in the wrapper chain. It is retained for source compatibility with callers that want the "original" error; new code should use errors.Is / errors.As / errors.Unwrap.
func (*TraceError) Error ¶
func (e *TraceError) Error() string
Error returns the contextual error message. When the error has a prefix, the prefix is prepended with a colon separator. Wrapped errors contribute their own prefixes via the Unwrap chain.
func (*TraceError) Format ¶
func (e *TraceError) Format(s fmt.State, verb rune)
Format implements fmt.Formatter.
%s, %v → Error() %q → quoted Error() %+v → Error() followed by RuntimeStack()
func (*TraceError) LogValue ¶
func (e *TraceError) LogValue() slog.Value
LogValue returns a slog.Value with the structured fields from Record. The raw stack PCs are omitted from the slog output to keep log lines compact; they remain available via JSON marshaling and the Stack method.
func (*TraceError) MarshalJSON ¶
func (e *TraceError) MarshalJSON() ([]byte, error)
MarshalJSON encodes the error as a Record.
func (*TraceError) Metadata ¶
func (e *TraceError) Metadata() *json.RawMessage
Metadata returns a deep copy of the caller-supplied metadata, or nil if none was set.
func (*TraceError) Prefix ¶
func (e *TraceError) Prefix() string
Prefix returns this wrapper's own prefix string. It does not include prefixes contributed by wrapped TraceErrors.
func (*TraceError) Record ¶
func (e *TraceError) Record() Record
Record returns a snapshot of the error suitable for structured output. The returned StackFrames and Stack slices are copies owned by the caller.
func (*TraceError) RuntimeStack ¶
func (e *TraceError) RuntimeStack() []byte
RuntimeStack returns a formatted byte slice describing the captured stack. The format is the package's own representation; it does not read source files from disk and is not guaranteed to match runtime/debug.Stack().
For TraceErrors built from a pre-formatted panic stack (FromPanic with a non-nil stack argument), RuntimeStack returns those raw bytes.
func (*TraceError) SetMetadata ¶
func (e *TraceError) SetMetadata(metadata *json.RawMessage) error
SetMetadata stores metadata on the error. Passing nil clears any previously stored metadata. Non-nil metadata is validated with json.Valid and cloned; invalid JSON is reported immediately and the previous value is left unchanged. SetMetadata is safe for concurrent use.
func (*TraceError) Stack ¶
func (e *TraceError) Stack() []uintptr
Stack returns a copy of the captured program counters. Callers may freely mutate the returned slice.
func (*TraceError) StackFrames ¶
func (e *TraceError) StackFrames() []StackFrame
StackFrames returns a copy of the resolved stack frame data. Frames are resolved lazily on first call. Subsequent calls reuse the cached frames and return a fresh copy each time.
func (*TraceError) Type ¶
func (e *TraceError) Type() string
Type returns a Go type string describing the underlying cause. For errors produced by ParsePanic or FromPanic it returns "panic". The empty string is returned when no cause is present. The result is diagnostic only and is not stable enough for domain control flow.
func (*TraceError) UnmarshalMetadata ¶
func (e *TraceError) UnmarshalMetadata(target any) error
UnmarshalMetadata decodes the stored metadata into target. It returns nil without touching target when no metadata is present.
func (*TraceError) Unwrap ¶
func (e *TraceError) Unwrap() error
Unwrap returns the immediate wrapped cause for use with errors.Is and errors.As.