• v1.0.0 c6ff8d0a3f

    Rene Nochebuena released this 2026-05-11 17:51:16 -06:00 | 1 commits to main since this release

    v1.0.0

    code.nochebuena.dev/go/xerrors

    Overview

    xerrors v1.0.0 commits the structured error API as stable. All v0.9.0 roadmap
    items are resolved. The module ships five convenience constructors covering the
    most-used error codes, a complete set of thirteen typed codes aligned with gRPC
    canonical status names, and the zero-import logz enrichment bridge that has been
    validated in production across sei-api and the httpauth middleware stack.

    What Changed Since v0.10.0

    New: Unauthorized and PermissionDenied convenience constructors

    func Unauthorized(msg string, args ...any) *Err
    func PermissionDenied(msg string, args ...any) *Err
    

    Completes the convenience constructor set. The five most-used codes now have
    dedicated constructors:

    Constructor Code
    InvalidInput(msg, args...) ErrInvalidInput
    NotFound(msg, args...) ErrNotFound
    Internal(msg, args...) ErrInternal
    Unauthorized(msg, args...) ErrUnauthorized (new)
    PermissionDenied(msg, args...) ErrPermissionDenied (new)

    Less-used codes continue to use New or Wrap directly.

    Roadmap items resolved

    Item Resolution
    Validate all codes in production Validated in sei-api and httpauth stack
    Codes registry / lookup helper Decision: No — transport mapping belongs in httputil
    ErrorContext() read-only contract Documented in godoc since v0.10.0
    Additional convenience constructors Unauthorized and PermissionDenied added
    Production validation of logz bridge Concurrent load validated

    Full API (stable)

    Code type — string alias; thirteen constants (ErrInvalidInput, ErrUnauthorized,
    ErrPermissionDenied, ErrNotFound, ErrAlreadyExists, ErrGone,
    ErrPreconditionFailed, ErrRateLimited, ErrCancelled, ErrInternal,
    ErrNotImplemented, ErrUnavailable, ErrDeadlineExceeded).
    HTTP mapping belongs to the transport layer; see httputil for the reference implementation.

    New(code, message) *Err — primary factory.

    Wrap(code, message, cause) *Err — wraps an existing error.

    InvalidInput(msg, args...) *ErrErrInvalidInput.

    NotFound(msg, args...) *ErrErrNotFound.

    Internal(msg, args...) *ErrErrInternal.

    Unauthorized(msg, args...) *ErrErrUnauthorized. (New in v1.0.0)

    PermissionDenied(msg, args...) *ErrErrPermissionDenied. (New in v1.0.0)

    (*Err).WithContext(key, value) *Err — attach a key-value field; chainable.

    (*Err).WithError(err) *Err — set or replace the cause; chainable.

    (*Err).WithPlatformCode(code) *Err — domain-level code for frontend i18n; chainable.

    (*Err).Code() Code, Message() string, PlatformCode() string,
    Fields() map[string]any, Detailed() string — accessors.

    (*Err).ErrorCode() string and (*Err).ErrorContext() map[string]any
    duck-type bridge for logz automatic enrichment; read-only contract on ErrorContext.

    (*Err).MarshalJSON(){"code":"...","platformCode":"...","message":"...","fields":{...}}.

    Migration from v0.10.0

    No breaking changes. The only additions are Unauthorized and PermissionDenied.

    go get code.nochebuena.dev/go/xerrors@v1.0.0
    
    Downloads
  • Rene Nochebuena released this 2026-03-25 16:43:42 -06:00 | 2 commits to main since this release

    v0.10.0

    code.nochebuena.dev/go/xerrors

    Overview

    xerrors provides a single structured error type — *Err — that carries a typed machine-readable Code, a human-readable message, an optional cause, and optional key-value context fields. It replaces ad-hoc string errors and sentinel variables with a consistent, JSON-serialisable error model that works uniformly across service boundaries, log pipelines, and HTTP transport layers. Error codes are stable wire values aligned with gRPC canonical status names, so they remain meaningful regardless of the transport protocol in use.

    What's New in v0.10.0

    Platform-level error codes

    Adds an optional PlatformCode to *Err for domain-specific error identity, decoupled from the transport-level Code.

    Motivation: The transport Code (e.g. NOT_FOUND, ALREADY_EXISTS) is intentionally generic — it drives HTTP status codes and gRPC status codes at the transport layer. This makes it unsuitable for driving per-case translations in a consuming frontend. A NOT_FOUND could mean an employee, a role, or a branch was not found — the frontend cannot distinguish them and falls back to a generic message.

    PlatformCode solves this cleanly by operating at the platform/domain layer, independently of transport:

    // Service layer — set a platform code on any actionable domain error
    return xerrors.New(xerrors.ErrNotFound, "employee not found").
        WithPlatformCode("EMPLOYEE_NOT_FOUND")
    
    return xerrors.New(xerrors.ErrAlreadyExists, "email already registered").
        WithPlatformCode("EMAIL_ALREADY_EXISTS")
    

    The JSON response includes both fields:

    {
      "code": "NOT_FOUND",
      "platformCode": "EMPLOYEE_NOT_FOUND",
      "message": "employee not found"
    }
    

    The consuming application (e.g. a Spanish-first frontend) maps platformCode to a translated message. code still drives the HTTP status. Both are independent.

    Platform codes are optional. Errors without a user-actionable meaning — 500 internal errors, infrastructure failures, authentication rejections — should not carry one. The frontend renders a generic fallback for those regardless.

    New API

    • (*Err).WithPlatformCode(code string) *Err — chainable builder; attaches a platform code
    • (*Err).PlatformCode() string — getter; returns the platform code or empty string if not set
    • MarshalJSON now includes "platformCode" when set, omits it otherwise (omitempty)

    Backwards compatibility

    No existing behaviour changed. All previous constructors, builder methods, and MarshalJSON output for errors without a platform code are identical. This is a purely additive change.

    What's Included

    • Code — string type alias for machine-readable error categories
    • ErrInvalidInput, ErrUnauthorized, ErrPermissionDenied, ErrNotFound, ErrAlreadyExists, ErrGone, ErrPreconditionFailed, ErrRateLimited, ErrCancelled, ErrInternal, ErrNotImplemented, ErrUnavailable, ErrDeadlineExceeded — thirteen typed code constants
    • Code.Description() — human-readable description for each code
    • *Err — structured error type implementing error, errors.Unwrap, json.Marshaler, ErrorCode(), and ErrorContext()
    • New(code, message) — primary factory constructor
    • Wrap(code, message, cause) — factory constructor that wraps an existing error as the cause
    • InvalidInput(msg, args...), NotFound(msg, args...), Internal(msg, args...) — convenience constructors for the three most common codes
    • (*Err).WithContext(key, value) — builder method to attach a key-value field; chainable
    • (*Err).WithError(err) — builder method to set or replace the cause; chainable
    • (*Err).WithPlatformCode(code) — builder method to attach a platform-level code; chainable (new)
    • (*Err).Code() — returns the typed Code
    • (*Err).Message() — returns the human-readable message string
    • (*Err).PlatformCode() — returns the platform code, or empty string (new)
    • (*Err).Fields() — returns a safe shallow copy of all attached context fields
    • (*Err).Detailed() — returns a verbose debug string including code, message, cause, and fields
    • (*Err).ErrorCode() — duck-type bridge satisfying logz's internal errorWithCode interface
    • (*Err).ErrorContext() — duck-type bridge satisfying logz's internal errorWithContext interface
    • (*Err).MarshalJSON() — JSON output as {"code":"...","platformCode":"...","message":"...","fields":{...}}

    Installation

    require code.nochebuena.dev/go/xerrors v0.10.0
    

    Design Highlights

    • Code and PlatformCode are intentionally separate concerns: Code is transport-layer identity (stable, bounded, maps to HTTP/gRPC status); PlatformCode is domain-layer identity (open-ended, consumer-defined, transport-agnostic).
    • Error codes are string type aliases with stable wire values matching gRPC canonical status names, making them safe to serialize, store, and compare across service versions.
    • *Err implements errors.Unwrap, enabling full stdlib errors.Is / errors.As cause-chain traversal without any custom helpers.
    • The logz integration is zero-import: ErrorCode() and ErrorContext() satisfy private duck-typed interfaces inside logz.
    • HTTP status mapping is deliberately excluded; that responsibility belongs to the transport layer.

    Changelog

    See CHANGELOG.md.

    Downloads
  • v0.9.0 3cc36801a1

    Rene Nochebuena released this 2026-03-18 13:12:17 -06:00 | 3 commits to main since this release

    v0.9.0

    code.nochebuena.dev/go/xerrors

    Overview

    xerrors provides a single structured error type — *Err — that carries a typed machine-readable Code, a human-readable message, an optional cause, and optional key-value context fields. It replaces ad-hoc string errors and sentinel variables with a consistent, JSON-serialisable error model that works uniformly across service boundaries, log pipelines, and HTTP transport layers. Error codes are stable wire values aligned with gRPC canonical status names, so they remain meaningful regardless of the transport protocol in use.

    What's Included

    • Code — string type alias for machine-readable error categories
    • ErrInvalidInput, ErrUnauthorized, ErrPermissionDenied, ErrNotFound, ErrAlreadyExists, ErrGone, ErrPreconditionFailed, ErrRateLimited, ErrCancelled, ErrInternal, ErrNotImplemented, ErrUnavailable, ErrDeadlineExceeded — twelve typed code constants
    • Code.Description() — human-readable description for each code
    • *Err — structured error type implementing error, errors.Unwrap, json.Marshaler, ErrorCode(), and ErrorContext()
    • New(code, message) — primary factory constructor
    • Wrap(code, message, cause) — factory constructor that wraps an existing error as the cause
    • InvalidInput(msg, args...), NotFound(msg, args...), Internal(msg, args...) — convenience constructors for the three most common codes
    • (*Err).WithContext(key, value) — builder method to attach a key-value field; chainable
    • (*Err).WithError(err) — builder method to set or replace the cause; chainable
    • (*Err).Code() — returns the typed Code
    • (*Err).Message() — returns the human-readable message string
    • (*Err).Fields() — returns a safe shallow copy of all attached context fields
    • (*Err).Detailed() — returns a verbose debug string including code, message, cause, and fields
    • (*Err).ErrorCode() — duck-type bridge satisfying logz's internal errorWithCode interface
    • (*Err).ErrorContext() — duck-type bridge satisfying logz's internal errorWithContext interface
    • (*Err).MarshalJSON() — JSON output as {"code":"...","message":"...","fields":{...}}

    Installation

    require code.nochebuena.dev/go/xerrors v0.9.0
    

    Design Highlights

    • Error codes are string type aliases with stable wire values matching gRPC canonical status names, making them safe to serialize, store, and compare across service versions (see docs/adr/ADR-001-typed-error-codes.md).
    • *Err implements errors.Unwrap, enabling full stdlib errors.Is / errors.As cause-chain traversal without any custom helpers (see docs/adr/ADR-002-stdlib-errors-compatibility.md).
    • The logz integration is zero-import: ErrorCode() and ErrorContext() satisfy private duck-typed interfaces inside logz, so passing an *Err to logger.Error automatically enriches the log record with error_code and context fields — without xerrors importing logz.
    • HTTP status mapping is deliberately excluded; that responsibility belongs to the transport layer, keeping this package dependency-free and protocol-agnostic.

    Known Limitations & Edge Cases

    • errors.As on a cause chain stops at the first *Err it encounters — if multiple *Err values are nested, only the outermost one's fields are surfaced in a single As call.
    • ErrorContext() returns the live internal fields map. Callers who receive an *Err and mutate the map returned by ErrorContext() will corrupt the error's state. Use Fields() for a safe copy.
    • Convenience constructors (InvalidInput, NotFound, Internal) cover only three of the twelve codes. Other codes require New or Wrap directly.
    • There is no HTTP status code mapping in this package. Applications must maintain their own Code → HTTP status table at the transport layer.
    • WithContext silently overwrites a field if the same key is set twice; no warning or error is produced.

    v0.9.0 → v1.0.0 Roadmap

    • Validate that all twelve codes have been exercised in at least one production service and that the mapping to HTTP status codes is stable and documented.
    • Consider adding a Codes registry or lookup helper so transport layers can map Code → HTTP status without each service maintaining its own switch statement.
    • Evaluate whether additional convenience constructors (e.g. PermissionDenied, Unauthorized) would reduce boilerplate enough to justify the addition.
    • Confirm that ErrorContext() returning the live map is acceptable in all log-enrichment paths, or document that it must be treated as read-only by callers.
    • Achieve production validation of the duck-type logz bridge under concurrent load.

    v0.9.0 rationale: The API is stable and intentional — designed through multiple architecture reviews and tested end-to-end via the todo-api POC (SQLite, RBAC, middleware stack, HTTP handlers). The module is not yet battle-tested in production for all edge cases, and the pre-1.0 designation preserves the option for minor API refinements based on real-world use.

    Downloads