go-errors provides categorized Go errors with rich internal state and
secure-by-default boundary output. Causes, metadata, validation values,
locations, and stacks remain available programmatically, but ordinary strings,
JSON, API responses, and logging do not expose them.
go get github.com/goliatone/go-errorsproviderErr := fmt.Errorf("provider rejected client_secret=%s", secret)
err := errors.Wrap(providerErr, errors.CategoryExternal, "payment failed").
WithCode(502).
WithTextCode("PAYMENT_PROVIDER_FAILED").
WithRequestID("request-123").
WithMetadata(map[string]any{"clientSecret": secret})
fmt.Println(err) // controlled summary; no cause or metadata
data, _ := json.Marshal(err) // PublicError JSON only
errors.LogBySeverity(logger, err) // controlled message and safe attributes
response, renderErr := err.ToPublicErrorResponse()
// response is safe and usable even when renderErr reports sanitization failure.Default public output contains classification, a controlled message, sanitized validation field information, correlation data, and severity. It omits source text, arbitrary metadata, validation values, timestamps, locations, and stacks.
Masking is defense in depth for selected structured values. It cannot reliably find credentials embedded in arbitrary prose, so source strings and unknown metadata are omitted rather than passed through a best-effort secret scanner.
Error remains the internal representation:
err.Source
err.Metadata
err.ValidationErrors
err.Location
err.StackTrace
errors.Is(err, cause)
errors.As(err, &target)
errors.Unwrap(err)Do not marshal the internal fields manually or send Source.Error() to clients
or normal logs. Use public output for API boundaries and an explicitly configured
diagnostic renderer for additional operational detail.
Renderers are immutable after construction and distinguish three contexts:
OutputPublic: API responses and standard JSON.OutputLog: controlled log messages and allowlisted attributes.OutputDiagnostic: explicitly selected, masked operational diagnostics.
Fields use FieldOmit, FieldInclude, FieldReplace, or FieldMask. Raw
pass-through is rejected for messages, sources, metadata, validation data,
locations, and stacks.
renderer, err := errors.NewRenderer(
errors.OutputPublic,
errors.WithMessageResolver(func(
context errors.OutputContext,
input errors.MessageInput,
) (string, error) {
if input.TextCode == "ACCOUNT_LOCKED" {
return "The account is temporarily unavailable.", nil
}
return "The request could not be completed.", nil
}),
)
public, renderErr := renderer.Public(errValue)Message hooks receive immutable value snapshots, not the original error.
renderer, err := errors.NewRenderer(
errors.OutputLog,
errors.WithMetadataAllowlist("provider", "attempt", "auth"),
errors.WithMetadataFilter(func(
context errors.OutputContext,
key string,
copiedValue any,
) (bool, error) {
return key != "auth" || copiedValue != nil, nil
}),
)
logRecord, renderErr := renderer.Log(errValue)Allowlisting happens before hooks and recursive masking. Hooks operate on output copies. An allowlist is not inferred from masking rules: unlisted metadata stays omitted.
Source and path information require dedicated replacement hooks:
renderer, err := errors.NewRenderer(
errors.OutputDiagnostic,
errors.WithMetadataAllowlist("provider_payload"),
errors.WithDiagnosticValidationValues(true),
errors.WithSourceSanitizer(func(input errors.SourceInput) (string, error) {
return "provider request failed", nil
}),
errors.WithPathSanitizer(func(value string) (string, error) {
return filepath.Base(value), nil
}),
)
diagnostic, renderErr := renderer.FormatDiagnostic(errValue)Without SourceSanitizer, source is omitted. Without PathSanitizer, location
and stack are omitted. Diagnostic DTOs are never used by Error.MarshalJSON or
standard response helpers.
Hook and masker failures return a usable safe result plus ErrSanitization.
Rejected fields are omitted or replaced with a fixed safe value; original input
is never restored.
renderer, err := errors.NewRenderer(
errors.OutputDiagnostic,
errors.WithSanitizationFailure(errors.SanitizationOmit, ""),
)
diagnostic, renderErr := renderer.Diagnostic(errValue)
if errors.Is(renderErr, errors.ErrSanitization) {
// Observe the failure; diagnostic remains safe.
}errors.LogBySeverity(logger, errValue) // fixed secure renderer
renderErr := errors.LogWithRenderer(logger, errValue, customLogRenderer)
attrs, renderErr := errors.ToSafeSlogAttributes(errValue)Default logs include category, numeric/text codes, severity, request ID, and validation field/count information. They omit causes, stacks, and arbitrary metadata. Critical and fatal errors do not implicitly append stack traces.
ErrorResponse carries *PublicError, not *Error:
response, renderErr := errValue.ToPublicErrorResponse()
data, _ := json.Marshal(response)ToErrorResponse(includeStack, stack) remains for source compatibility, but its
legacy parameters are ignored. Public responses never contain stacks.
Public validation entries contain only sanitized field and message values. The
internal FieldError.Value remains available programmatically and is omitted by
default. Diagnostic validation values, when explicitly enabled, are fully
redacted.
The package separates three operations:
wrapped := errors.Wrap(cause, errors.CategoryExternal, "provider request failed")
contextual := errors.AddContext(wrapped, "create invoice")
reclassified := errors.Reclassify(contextual, errors.CategoryOperation, "invoice failed")Wrapcreates a new outer error and honors the supplied category.AddContextcreates an outer error while preserving structured classification.Reclassifyexplicitly changes category while retaining the prior chain.
All preserve errors.Is, errors.As, and errors.Unwrap traversal.
Use factories when location capture must differ between consumers:
production, _ := errors.NewFactory(errors.WithLocationCapture(false))
diagnostic, _ := errors.NewFactory(errors.WithLocationCapture(true))
prodErr := production.New("operation failed", errors.CategoryOperation)
diagErr := diagnostic.Wrap(cause, errors.CategoryExternal, "provider failed")SetLocationCapture remains an atomic compatibility setting for package
constructors. Direct writes to EnableLocationCapture are deprecated and are no
longer observed. Verbose and IsDevelopment are also deprecated as output
controls; use renderer policies instead.
Collectors and retryable errors retain their existing programmatic semantics:
collector := errors.NewCollector(
errors.WithMaxErrors(100),
errors.WithStrictMode(false),
)
collector.AddValidation("email", "invalid format")
collector.AddRetryable(cause, errors.CategoryExternal, "provider unavailable")
response := collector.ToErrorResponse(false)
collector.LogErrors(logger)Responses and logs use the same safe renderers as individual errors. Merge metadata and raw validation data remain internal.
Error.Error()no longer includes source, validation messages, metadata counts, location, or stack data.Error.MarshalJSON()now emits the public DTO shape.ErrorResponse.Errorchanged from*Errorto*PublicError.ErrorWithStack()no longer emits an unsanitized stack; use an explicit diagnostic renderer.Wrapnow honors its supplied category for structured errors and does not concatenate messages. UseAddContextto preserve classification.- Normal logging omits arbitrary metadata and validation messages.
Verbose,IsDevelopment, and direct writes toEnableLocationCaptureno longer control output.go-maskerv0.2.0 is used through a dedicated immutable secure instance; the package does not mutatemasker.Default.
These changes are intended for the next pre-1 minor release because they alter the default wire shape and formatting behavior.
MIT