Skip to content
goliatonePublic

About

Go error utilities

Resources

Stars

0 stars

Watchers

1 watching

Forks

Latest commit

 

History

123 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Go Errors

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.

Install

go get github.com/goliatone/go-errors

Secure defaults

providerErr := 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.

Internal state and cause chains

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.

Output contexts

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.

Controlled messages

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.

Allowlisted, masked metadata

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.

Masked diagnostics

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.

Fail-closed behavior

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.
}

Logging

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.

Responses and validation

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.

Wrapping semantics

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")
  • Wrap creates a new outer error and honors the supplied category.
  • AddContext creates an outer error while preserving structured classification.
  • Reclassify explicitly changes category while retaining the prior chain.

All preserve errors.Is, errors.As, and errors.Unwrap traversal.

Immutable factories

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

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.

Migration from v0.11

  • Error.Error() no longer includes source, validation messages, metadata counts, location, or stack data.
  • Error.MarshalJSON() now emits the public DTO shape.
  • ErrorResponse.Error changed from *Error to *PublicError.
  • ErrorWithStack() no longer emits an unsanitized stack; use an explicit diagnostic renderer.
  • Wrap now honors its supplied category for structured errors and does not concatenate messages. Use AddContext to preserve classification.
  • Normal logging omits arbitrary metadata and validation messages.
  • Verbose, IsDevelopment, and direct writes to EnableLocationCapture no longer control output.
  • go-masker v0.2.0 is used through a dedicated immutable secure instance; the package does not mutate masker.Default.

These changes are intended for the next pre-1 minor release because they alter the default wire shape and formatting behavior.

License

MIT

About

Go error utilities

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

⌂ 换网址