errorchan

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jun 5, 2026 License: MIT Imports: 7 Imported by: 0

README

go-errorchan

A pun that finally compiles in Go: -chan, the cutesy anime honorific, and chan, the channel your errors flow through. go-errorchan personifies your errors as an anime character who reacts to your code failing — while keeping the real error completely intact and debuggable.

CI Go Reference Go Report Card

import "github.com/klobucar/go-errorchan"

The headline feature is Styled, which restyles every error flowing through a channel — because in Go, chan was a channel all along.

Install

go get github.com/klobucar/go-errorchan

Standard library only. Zero third-party runtime dependencies.

Modes

Three personality modes change how an error is delivered. The underlying error is byte-for-byte identical in every mode.

Mode Vibe Sample
dere (default) sweet, flustered, apologetic, takes the blame evewything was finye u-untiw this *errors.errorString*... g-gomen nyasai >_< — connection refused
tsun annoyed at you, blames your code, grudgingly helpful tch, a *errors.errorString*. finye, hewe's the detaiw. don't make me wepeat it >:( — EOF
yan unsettlingly affectionate about your failures (do not use in prod) anyothew *errors.errorString*... good. t-the mowe you faiw, the mowe you'we minye (◡‿◡✿) — permission denied

Each mode has its own intro lines and kaomoji, run through a shared phonetic transform (r/lw, n+vowel → ny, and seed-driven stutters for the heavier modes).

errorchan.SetMode("tsun")  // global default; concurrency-safe
mode := errorchan.Mode()   // "tsun"

The one rule

The real error is never hidden or mangled. The persona only wraps around it:

  • errors.Is and errors.As see straight through (StyledError implements Unwrap).
  • The original is exposed on the value: styled.Original (the error) and styled.OriginalMessage (its message, verbatim).
  • Error() keeps the original message verbatim after a clear separator, always on a single line so it never wrecks your logs.
  • Re-wrapping preserves %w semantics, so callers' sentinel and type checks keep passing.
styled := errorchan.Wrap(io.EOF, errorchan.WithMode("tsun"))
errors.Is(styled, io.EOF)   // true
styled.OriginalMessage      // "EOF"

API

// Raw phonetic transform, exposed.
errorchan.Uwuify("really cool")                       // "weawwy coow"

// Wrap a single error: *StyledError, Unwrap() == err.
styled := errorchan.Wrap(err, opts...)

// THE PUN: restyle every error flowing through a channel.
out := errorchan.Styled(in <-chan error, opts...)     // returns <-chan error

// Restyle a recovered panic into a named error return.
func doThing() (err error) {
    defer errorchan.Recover(&err, opts...)
    // ...
}

// Style error-valued attributes in slog records.
h := errorchan.NewSlogHandler(base, opts...)
log := slog.New(h)
Options

Configuration uses the functional-options pattern:

  • WithMode(string) — override the mode for this operation.
  • WithoutType() — suppress the wrapped error's Go type in the framing (the type slot becomes a neutral, uwuified noun). Useful when the framing is shown to end users who shouldn't see Go internals; the wrapped error and its verbatim message are unaffected.
  • WithSeed(int64) — deterministic output from a fresh, per-operation source (safe to reuse across goroutines).
  • WithRand(*rand.Rand) — deterministic output from a source you own (not safe to share across goroutines).

All randomness flows through the injected source, so seeded output is fully reproducible — which is exactly how the test suite pins every example.

errorchan.Uwuify("really cool", errorchan.WithSeed(1)) // always "weawwy coow"

The channel surface

in := make(chan error, 2)
in <- errors.New("timeout")
in <- errors.New("refused")
close(in)

for err := range errorchan.Styled(in, errorchan.WithMode("dere")) {
    fmt.Println(err)
}

Styled resolves its config once and shares a single source across the stream, so output varies error-to-error yet stays reproducible under a seed. nil values pass through as nil (never a non-nil wrapper around a nil error).

Design

The package is built in layers, each independent of the ones above it:

  1. Phonetic transform (transform.go) — deterministic given an injected *rand.Rand, with no knowledge of errors or personas.
  2. Persona layer (persona.go) — unexported data mapping each mode to its framing lines and kaomoji.
  3. Usage surfaces (Wrap, Styled, Recover, NewSlogHandler) — thin shells over one shared styling core.

Development

go test -race -cover ./...   # ~100% coverage, race-clean
gofmt -l .                   # formatting
go vet ./...                 # vet
golangci-lint run ./...      # strict lint (see .golangci.yml)
govulncheck ./...            # vulnerability scan

CI runs all of the above across a Go version matrix on every push and PR.

License

MIT

Documentation

Overview

Package errorchan personifies Go errors as an anime character who reacts to your code failing. The name is wordplay that finally lands in Go: "-chan", the cutesy Japanese honorific, and "chan", the channel.

Errors get a personality mode that changes how they are delivered, while the real error information stays intact and debuggable. The persona only ever wraps around an error; it never hides or mangles it.

Modes

Three modes shape the delivery (the underlying error is identical in each):

  • ModeDere (default): sweet, flustered, apologetic, takes the blame.
  • ModeTsun: annoyed at you, blames your code, grudgingly helpful anyway.
  • ModeYan: unsettlingly affectionate about your failures. Do not use in production.

The current global default is read with Mode and set with SetMode; both are safe for concurrent use.

Preserving the error

Every StyledError keeps the original message verbatim, appended after a clear separator on a single line, and implements Unwrap so that errors.Is, errors.As, and errors.Unwrap keep working through the persona:

styled := errorchan.Wrap(err, errorchan.WithMode(errorchan.ModeTsun))
errors.Is(styled, io.EOF)        // still true
styled.Original                  // the untouched original error
styled.OriginalMessage           // err.Error(), captured verbatim

Surfaces

The personality can be applied through several surfaces that all share one styling core: Wrap for a single error, Styled to restyle every error flowing through a channel (the pun), Recover to restyle a recovered panic inside a deferred call, and NewSlogHandler to style error-valued slog attributes.

Hiding the Go type

By default the framing names the wrapped error's Go type (for example *errors.errorString*) to aid debugging. Pass WithoutType to replace that slot with a neutral noun, run through the same phonetic transform as the rest of the framing, for cases where the framing is shown to end users:

styled := errorchan.Wrap(err, errorchan.WithoutType())
styled.OriginalMessage           // still the verbatim message
errors.Is(styled, io.EOF)        // still works

Determinism

All randomness (intro choice, kaomoji choice, stutter placement) flows through an injectable source. Pass WithSeed or WithRand for fully reproducible output, which is what the test suite relies on.

Index

Examples

Constants

View Source
const (
	// ModeDere is the default: sweet, flustered, apologetic, and takes the
	// blame for the failure.
	ModeDere = "dere"
	// ModeTsun is tsundere: annoyed at you, blames your code, and is grudgingly
	// helpful anyway.
	ModeTsun = "tsun"
	// ModeYan is yandere: unsettlingly affectionate about your failures. Do not
	// use in production.
	ModeYan = "yan"
)

The supported personality modes. Pass them to SetMode or WithMode.

Variables

This section is empty.

Functions

func Mode

func Mode() string

Mode returns the current global default mode. It is safe to call from multiple goroutines.

func NewSlogHandler

func NewSlogHandler(base slog.Handler, opts ...Option) slog.Handler

NewSlogHandler wraps base so that any error-valued attribute in a log record is restyled with the configured mode before being passed on. Non-error attributes are left untouched, and attributes nested in groups are styled recursively.

Because each error is styled independently, pass WithSeed for reproducible output. Avoid WithRand here: a handler may be invoked from many goroutines at once, and a shared *rand.Rand is not safe for concurrent use.

func Recover

func Recover(errp *error, opts ...Option)

Recover restyles a recovered panic into errp. Call it deferred at the top of a function that uses a named error return:

func doThing() (err error) {
    defer errorchan.Recover(&err)
    // ... code that may panic ...
}

If no panic is in flight Recover does nothing and leaves errp untouched. On a panic it recovers the value, converts it to an error (a panicked error is used directly so its chain and sentinel identity survive), styles it, and stores it through errp. A nil errp simply swallows the panic.

Example
package main

import (
	"errors"
	"fmt"

	"github.com/klobucar/go-errorchan"
)

func main() {
	doThing := func() (err error) {
		defer errorchan.Recover(&err, errorchan.WithMode(errorchan.ModeTsun), errorchan.WithSeed(2))
		panic(errors.New("nil pointer somewhere"))
	}

	err := doThing()
	fmt.Println(err)
}
Output:
a *errors.errorString*?! sewiouswy? I'm nyot hewping because I wike you ow anything ヽ(`Д´)ノ — nil pointer somewhere

func SetMode

func SetMode(mode string) error

SetMode sets the global default mode used when no WithMode option is given. It returns an error for an unknown mode and leaves the current mode unchanged in that case. It is safe to call from multiple goroutines.

Example
package main

import (
	"fmt"

	"github.com/klobucar/go-errorchan"
)

func main() {
	// Set the global default; subsequent Wrap calls use it unless overridden.
	previous := errorchan.Mode()
	defer func() { _ = errorchan.SetMode(previous) }()

	if err := errorchan.SetMode(errorchan.ModeYan); err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("mode is now:", errorchan.Mode())
}
Output:
mode is now: yan

func Styled

func Styled(in <-chan error, opts ...Option) <-chan error

Styled is the pun: it restyles every error flowing through a channel. It reads errors from in, wraps each non-nil error in the configured mode, and forwards the result on the returned channel. The output channel is closed once in is drained and closed.

A single random source is resolved once and shared across the stream, so with WithSeed or WithRand the sequence of styled errors is reproducible while still varying error-to-error. nil values are forwarded unchanged (a nil error stays a nil error, never a non-nil wrapper around nil).

Example
package main

import (
	"errors"
	"fmt"

	"github.com/klobucar/go-errorchan"
)

func main() {
	in := make(chan error, 2)
	in <- errors.New("timeout")
	in <- errors.New("refused")
	close(in)

	for err := range errorchan.Styled(in, errorchan.WithMode(errorchan.ModeDere), errorchan.WithSeed(5)) {
		fmt.Println(err)
	}
}
Output:
a *errors.errorString* came o-out... I-I didn't mean to wet it happen (。•́︿•̀。) — timeout
s-snyiffwe... a *errors.errorString*... I'l-ww twy hawdew n-nyext t-time, p-pwomise (。•́︿•̀。) — refused

func Uwuify

func Uwuify(s string, opts ...Option) string

Uwuify applies the raw phonetic transform to s and returns the result, for example "really cool" becomes "weawwy coow". The active mode selects the transform intensity (heavy for ModeDere and ModeYan, light for ModeTsun); pass WithSeed or WithRand for deterministic output.

Example
package main

import (
	"fmt"

	"github.com/klobucar/go-errorchan"
)

func main() {
	// WithSeed makes the phonetic transform reproducible.
	fmt.Println(errorchan.Uwuify("really cool", errorchan.WithSeed(1)))
}
Output:
weawwy coow

Types

type Option

type Option func(*config)

Option configures a styling operation. Options are applied with the functional-options pattern and are accepted by every public surface (Uwuify, Wrap, Styled, Recover, NewSlogHandler).

func WithMode

func WithMode(mode string) Option

WithMode selects the personality mode for this operation, overriding the global default from Mode. An unrecognized mode falls back to ModeDere.

func WithRand

func WithRand(r *rand.Rand) Option

WithRand makes the output deterministic by using the supplied source. Unlike WithSeed, the same *rand.Rand is shared by every operation that resolves this option; callers are responsible for not using it concurrently from multiple goroutines.

func WithSeed

func WithSeed(seed int64) Option

WithSeed makes the output deterministic by deriving a fresh random source from seed. Each operation that resolves this option gets its own source, so it is safe to reuse the same seeded option across goroutines.

func WithoutType

func WithoutType() Option

WithoutType suppresses the error's Go type in the persona framing. Normally the framing names the wrapped type (for example *errors.errorString*) to aid debugging; with this option the type slot is replaced by a neutral noun that is run through the same phonetic transform as the rest of the framing. The wrapped error and its verbatim message are unaffected. Useful when the framing is shown to end users who should not see Go internals.

type StyledError

type StyledError struct {
	// Original is the untouched error that was wrapped.
	Original error
	// OriginalMessage is Original.Error() captured at wrap time, preserved
	// verbatim.
	OriginalMessage string
	// contains filtered or unexported fields
}

StyledError wraps an error with personality framing while keeping the original error fully intact and reachable. It implements error and Unwrap, so errors.Is, errors.As, and errors.Unwrap all see through to the wrapped error.

func Wrap

func Wrap(err error, opts ...Option) *StyledError

Wrap returns err styled in the configured mode, or nil if err is nil. The returned *StyledError unwraps to err, so sentinel and type checks against the result keep working.

Example
package main

import (
	"errors"
	"fmt"
	"io"

	"github.com/klobucar/go-errorchan"
)

func main() {
	styled := errorchan.Wrap(io.EOF, errorchan.WithMode(errorchan.ModeTsun), errorchan.WithSeed(1))

	// The persona wraps the error, but the original is fully intact.
	fmt.Println(styled)
	fmt.Println("errors.Is(styled, io.EOF):", errors.Is(styled, io.EOF))
	fmt.Println("original:", styled.OriginalMessage)
}
Output:
tch, a *errors.errorString*. finye, hewe's the detaiw. don't make me wepeat it >:( — EOF
errors.Is(styled, io.EOF): true
original: EOF
Example (Dere)
package main

import (
	"errors"
	"fmt"

	"github.com/klobucar/go-errorchan"
)

func main() {
	err := errorchan.Wrap(errors.New("connection refused"), errorchan.WithSeed(7))
	fmt.Println(err)
}
Output:
evewything was finye u-untiw this *errors.errorString*... g-gomen nyasai >_< — connection refused

func (*StyledError) Error

func (e *StyledError) Error() string

Error returns the persona framing followed by the original message, separated by [separator] on a single line. The original message is preserved verbatim.

func (*StyledError) Framing

func (e *StyledError) Framing() string

Framing returns just the persona framing (intro plus kaomoji) without the original message, which is occasionally handy for custom formatting.

func (*StyledError) Mode

func (e *StyledError) Mode() string

Mode reports the personality mode used to render this error.

func (*StyledError) Unwrap

func (e *StyledError) Unwrap() error

Unwrap returns the original wrapped error, keeping %w-style chains and errors.Is/errors.As working through the persona.

Jump to

Keyboard shortcuts

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