appError

package
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Sep 18, 2026 License: MIT Imports: 1 Imported by: 0

README

appError — Usage Guide

appError is a small, dependency-free error type for Go. It gives you a single Typ (importable as appError.Typ and intended to be read as App Error Type) value that carries a severity level, a stable code (an "LMID" string), a human message, optional HTTP / developer metadata, and an optional wrap chain. It is designed to be returned by any function and to play nicely with the standard library's error-wrapping machinery (fmt.Errorf("... %w", ...), errors.Is, errors.As, errors.Unwrap).

Notes:
  1. How this guide maps to tests. Every code example below is covered by a test in appError_test.go. The test name is cited in the code fence so you can jump straight to the executable specification. If you change an example, update the matching test (and vice versa).
  2. Crafted by a human, tested and documented by AI: Most of the content here was written by a Local AI. I have tested the document multiple times and have gone through the tests myself. No AI was used for the main appError.go though. If there are errors, please let me know (or just raise a PR).
  3. Feature Completeness: I do not intend to add new features to this package. It is feature-complete and stable as far as I see it. If you feel like these should be something more or something different, let's discuss.

Table of contents

  1. The Typ
  2. Level — severity
  3. Typ — the error type
  4. Constructing errors
  5. Rendering: Error() / String()
  6. Inspecting errors
  7. Standard-library interop (%w, errors.Is, errors.As)
  8. Design notes & caveats
  9. Test index

The Typ

There are essentially two things in this package:

  • Typ — the error itself. It implements the error interface (Error() string), so it can be returned from any func(...) (T, error).
  • Level — an enum of severity values with an extra Unknown.
// `Typ` satisfies the standard `error` interface.
var _ error = appError.Typ{}            // compile-time assertion
var _ error = appError.NewError(appError.Err, "1", "m")

Covered by TestTypSatisfiesErrorInterface.


Level — severity

const (
    Panic   Level = 10  // FATAL (recovered or not)
    Alert   Level = 8   // recoverable, but must be alerted (email/webhook)
    Err     Level = 6   // recoverable error
    Warning Level = 4   // not an error, but must be looked into
    Notice  Level = 3   // not an error, but should be improved
    Info    Level = 2   // plain information
    Unknown Level = 0   // no level / not set
    Debug   Level = -4  // debug
)

NOTE: The severety levels are indicative and can be adjusted based on the application's needs. The numeric values are chosen to allow sorting of severity levels.

Methods
Method Purpose Example Test
(Level).String() full label, e.g. "Error" Err.String() → "Error" TestLevelString
(Level).ShortStr() single-char label, e.g. "E" Err.ShortStr() → "E" TestLevelShortStr
(Level).Int() / .Int8() numeric value as int/int8 Err.Int() → 8 TestLevelIntConversions
FromShortString(s) parse a single-char label back to a Level FromShortString("E") → Err TestFromShortString
Example
// Round-trip a level through its short string:
for _, lvl := range []appError.Level{
    appError.Panic, appError.Alert, appError.Err,
    appError.Warning, appError.Notice, appError.Info,
    appError.Unknown, appError.Debug,
} {
    got := appError.FromShortString(lvl.ShortStr())
    // got == lvl for all eight named levels.
}

Covered by TestFromShortStringRoundTrip, TestFromShortString.

Note: Level.String() returns "Unknown" for any value with no dedicated label (e.g. Level(1), Level(5), or negatives other than Debug), so an unknown level can no longer be masked as "Info". FromShortString is consistent with this (unrecognized → Unknown). See Design notes & caveats.


Typ — the error type

type Typ struct {
    Level            Level   // severity
    Code             string  // stable "LMID" code (e.g. "123456")
    Message          string  // human-readable message
    HttpResponseCode int     // HTTP status, when this is a network error
    DevMsg           string  // message meant for developers only
    WrappedError     *Typ    // the inner (wrapped) error, if any
    ExtraData        string  // free-form extra data (errors are values, per Rob Pike)
}
Example — a plain error value
e := appError.Typ{
    Level:   appError.Err,
    Code:    "123456",
    Message: "something broke",
}
_ = e.Error() // "E#123456: something broke"

Covered by TestTypErrorAndString.


Constructing errors

There are four ways to build a Typ.

1. Struct literal

The most direct way. Use this for a simple, un-wrapped error.

e := appError.Typ{Level: appError.Err, Code: "123456", Message: "something broke"}

Covered by TestTypErrorAndString.

2. NewError — with a variadic wrap chain

This is probably the way you would create most of your errors. It takes a Level, a Code, a Message, and any number of inner Typ values to wrap. NewError builds a Typ and folds any variadic Typ arguments into a WrappedError chain in the order supplied: the first argument becomes the outermost wrapped node and the last becomes the innermost leaf.

// No inner error -> 1-node (no chain)
e := NewError(Err, "123456", "msg")

// One inner error -> 2-node chain: outer -> inner
inner := appError.Typ{Level: appError.Panic, Code: "P1", Message: "inner"}
outer := appError.NewError(appError.Err, "E1", "outer", inner)
// chain: E1 -> P1

// Two inner errors -> 3-node chain: top -> a -> b (a is outermost, b is the leaf)
a := appError.Typ{Level: appError.Warning, Code: "AAA", Message: "a"}
b := appError.Typ{Level: appError.Warning, Code: "BBB", Message: "b"}
top := appError.NewError(appError.Err, "TOP", "top", a, b)
// chain: TOP -> AAA -> BBB

// Three inner errors -> 4-node chain: TOP -> XXX -> YYY -> ZZZ
x := appError.Typ{Level: appError.Warning, Code: "XXX", Message: "x"}
y := appError.Typ{Level: appError.Warning, Code: "YYY", Message: "y"}
z := appError.Typ{Level: appError.Warning, Code: "ZZZ", Message: "z"}
e3 := appError.NewError(appError.Err, "TOP", "top", x, y, z)
// chain: TOP -> XXX -> YYY -> ZZZ

Covered by TestNewErrorNoWrap, TestNewErrorSingleWrap, TestNewErrorMultiWrapChain, TestNewErrorThreeVariadics.

Guarantees you can rely on:

  • No mutation of inputs. NewError copies each variadic and never sets a WrappedError on the caller's values.

    Covered by TestNewErrorDoesNotMutateInput.

  • The chain always terminates (no cycles, no duplication). This is guarded under a timeout so a regression that re-introduces a non-terminating chain fails fast instead of hanging the suite.

    Covered by TestNewErrorChainTerminates.

  • No blank-filtering. Both NewError and NewNetworkError wrap a blank variadic like any other (see Design notes & caveats).

    Covered by TestNewErrorBlankVariadic.

3. NewNetworkError — HTTP-aware construction

NewNetworkError adds an HTTP status code and a developer-only message, and validates the HTTP code. If the code is outside 100..599, it returns a synthetic Alert (code 1DJ1A2, HTTP 500) that wraps the original request so the bad value is preserved for debugging.

// Valid HTTP code -> a 2-node chain, keeping the supplied inner error.
inner := appError.Typ{Level: appError.Panic, Code: "P1", Message: "inner"}
ne := appError.NewNetworkError(404, appError.Warning, "888888", "not found", "dev says", inner)
// ne.HttpResponseCode == 404 ; chain: 888888 -> P1

// Invalid HTTP code (999) -> synthetic Alert, HTTP 500, wrapping the bad request.
bad := appError.NewNetworkError(999, appError.Warning, "888888", "msg", "dev")
// bad.Level == Alert ; bad.Code == "1DJ1A2" ; bad.HttpResponseCode == 500
// chain: 1DJ1A2 -> 888888

Covered by TestNewNetworkErrorValid, TestNewNetworkErrorInvalidCode, TestNewNetworkErrorInvalidCodePreservesInnerWrap, TestNewNetworkErrorInvalidCodeNoWrap.

Note: NewNetworkError now folds all variadics exactly like NewError (in order, first = outermost, last = leaf) and — like NewError — performs no blank-filtering (a blank variadic is wrapped). See Design notes & caveats. Covered by TestNewNetworkErrorValidBlankWrap.

4. BlankError() and .Wrap()
  • BlankError() returns a fresh, empty Typ (Level Unknown, empty code and message). It returns a copy, so mutating the result can't corrupt the sentinel.
  • .Wrap(inner) returns a copy of the receiver with inner attached as the WrappedError; the receiver is left untouched.
blank := appError.BlankError()     // Level: Unknown, Code: "", Message: ""
// blank.IsBlank() == true

inner := appError.Typ{Level: appError.Panic, Code: "P", Message: "p"}
outer := appError.Typ{Level: appError.Err, Code: "E", Message: "e"}
wrapped := outer.Wrap(inner)         // wrapped.WrappedError points at a copy of `inner`
// outer.WrappedError is still nil (Wrap does not mutate the receiver)

Covered by TestBlankErrorFunction, TestWrapMethod.


Rendering: Error() / String()

Error() and String() are identical. The format is "<ShortStr>#<Code>: <Message>", and each wrapped error is appended on its own line with a "[Wraps error ==>]" separator:

W#O1: outer
   [Wraps error ==>]
E#M1: mid
   [Wraps error ==>]
P#P1: inner
inner := appError.Typ{Level: appError.Panic, Code: "P1", Message: "inner"}
mid := appError.Typ{Level: appError.Err, Code: "M1", Message: "mid", WrappedError: &inner}
outer := appError.Typ{Level: appError.Warning, Code: "O1", Message: "outer", WrappedError: &mid}
_ = outer.String() // multi-line, as shown above

Covered by TestTypStringWithWrapped, TestTypStringMultiLevel.


Inspecting errors

Method Returns Meaning
IsBlank() / IsNotBlank() bool Whether the Typ is "empty" (Level Unknown, code "" or legacy "000000", empty Message and DevMsg).
WrapsErrorCode(code) bool Whether code appears anywhere in the chain (self first, then the wrap chain).
WrapsErrorLevel(lvl, checkCurrent) bool Whether lvl appears in the chain. If checkCurrent is true, the receiver's own level counts.
IsBlankNetworkError() / IsNotBlankNetworkError() bool IsBlank() and HttpResponseCode == 0.
// Blank detection
_ = appError.BlankError().IsBlank()              // true
_ = appError.Typ{Level: appError.Unknown}.IsBlank() // true
_ = appError.Typ{Level: appError.Err}.IsBlank()   // false (a real level is non-blank)

// Chain queries
e3 := appError.NewError(appError.Err, "TOP", "top", x, y, z) // TOP -> XXX -> YYY -> ZZZ
_ = e3.WrapsErrorCode("ZZZ")    // true  (deep in the chain)
_ = e3.WrapsErrorCode("NOPE")   // false
_ = e3.WrapsErrorLevel(appError.Warning, true) // true (outermost is Warning)

// Network blankness
_ = appError.Typ{}.IsBlankNetworkError()                 // true  (zero value)
_ = appError.Typ{Level: appError.Err, HttpResponseCode: 404}.IsBlankNetworkError() // false

Covered by TestIsBlank, TestNetworkErrorBlankness, TestWrapsErrorCode, TestWrapsErrorLevel, TestWrapsErrorLevelNoInfiniteLoop.

Note: WrapsErrorLevel (and the WrapsErrorCode recursion) are guarded by a timeout in the tests. This is a regression guard against the historic "re-read WrappedError without advancing" infinite loop. See PROBS.md.


Standard-library interop

This is the part that makes Typ a drop-in error you can return from anywhere.

Wrapping with fmt.Errorf + %w

Wrap a Typ inside any other error using %w, and it stays recoverable:

base := appError.Typ{Level: appError.Err, Code: "999001", Message: "db failure"}
err := fmt.Errorf("could not save user: %w", base)

var recovered appError.Typ
if errors.As(err, &recovered) {
    // recovered == base (the outermost Typ)
    _ = recovered.Code    // "999001"
    _ = recovered.Message // "db failure"
}

// A Typ value also works with errors.Is when it is the outermost node:
_ = errors.Is(err, base) // true

Covered by TestWrapTypInsideFmtError.

The internal wrap chain and errors.*

Typ exposes Unwrap(), so errors.Is / errors.As / errors.Unwrap can walk the whole internal WrappedError chain, not just a single level. Unwrap() returns a Typ value (via *e.WrappedError), so the entire chain is homogeneous Typ values — there are no *Typ nodes to worry about:

Query What it matches
errors.As(err, &Typ) the outermost Typ (first match in the chain)
errors.As(err, &(*Typ)) nothing — the chain has no *Typ nodes
errors.Is(err, someTypValue) any node equal to someTypValue (by value)
errors.Is(err, someTypPtr) nothing — the chain has no *Typ nodes
// Build a clean 3-node chain by hand (all Typ values once unwrapped)
inner := &appError.Typ{Level: appError.Panic, Code: "111111", Message: "deepest"}
mid := &appError.Typ{Level: appError.Err, Code: "222222", Message: "middle", WrappedError: inner}
top := appError.Typ{Level: appError.Warning, Code: "333333", Message: "top", WrappedError: mid}

err := fmt.Errorf("ctx: %w", top)

// Walk the whole chain with errors.Unwrap — every node is a Typ value:
var cur error = err
for cur != nil {
     // ...
    cur = errors.Unwrap(cur)
}

Covered by TestUnwrapReturnsInnerValue, TestUnwrapNilWhenNotWrapped, TestErrorsAsTypValueTarget, TestErrorsAsTypPtrTarget, TestErrorsIsTypValueTarget, TestErrorsIsTypPtrTarget, TestErrorsIsUnwrapDepth.


Design notes

A few things that might look a bit "confusing" to some people (but are in fact intentional):

  1. IsBlank() ignores HttpResponseCode and WrappedError - A Typ with no valid Code and no valid Message at the top level is not considered a valid (non-blank) error, even if it carries an HTTP code or wraps another error. So IsBlank() looks only at Level / Code / Message / DevMsg. This is not true for IsBlankNetworkError, which additionally requires HttpResponseCode == 0.

  2. IsBlankNetworkError requires HttpResponseCode != 0 - A Typ with an HTTP code but otherwise empty content is "blank error" yet not a "blank network error". Please be aware of the split. Why is it this way - Sometimes, we need to show an error to the client with an HTTP code but no message (such as a plain 403, or a 301) even when there is actually nothing wrong in the backend. This behavior helps in that.

  3. String() indentation is fixed. The "[Wraps error ==>]" separator uses a constant indent that does not grow with depth. This is intentional; for long chains it makes sure that the error chain is visually readable and traceable, especially on screens with limited width. I am planning to add a depth-based number in front of the "[Wraps error ==>]" separator in future versions to make it even more clear.

  4. Level.String() returns "Unknown" for unrecognized levels. Any level without a dedicated label (e.g. Level(1), Level(5), negatives other than Debug) renders as "Unknown" (not "Info"), so a genuinely unknown level can no longer be masked as information. FromShortString is consistent with this (unknown → Unknown).


Test index

Test What it pins down
TestLevelString Level.String() labels, incl. the "Unknown" fallback
TestLevelShortStr Level.ShortStr() single-char labels
TestLevelIntConversions Level.Int() / Level.Int8()
TestFromShortString FromShortString parsing incl. Unknown fallback
TestFromShortStringRoundTrip all eight named levels round-trip
TestTypErrorAndString Error()/String() for a plain Typ
TestTypStringWithWrapped single-wrap String() rendering
TestTypStringMultiLevel multi-level String() rendering (order + marker count)
TestBlankErrorFunction BlankError() is blank, empty, stable by value
TestWrapMethod .Wrap() attaches a copy and does not mutate the receiver
TestIsBlank blankness rules (level/code/message/devmsg/wrap)
TestNetworkErrorBlankness IsBlankNetworkError / IsNotBlankNetworkError
TestWrapsErrorCode self + chain code lookup (no infinite loop)
TestWrapsErrorLevel self + chain level lookup
TestWrapsErrorLevelNoInfiniteLoop regression guard for the old infinite-loop bug
TestNewErrorNoWrap NewError with no variadic
TestNewErrorSingleWrap 1 variadic → clean 2-node chain (no duplication)
TestNewErrorMultiWrapChain 2 variadics → 3-node chain, first = outermost
TestNewErrorThreeVariadics 3 variadics → 4-node chain, correct order
TestNewErrorBlankVariadic NewError does not blank-filter
TestNewErrorChainTerminates wrapped NewError chain terminates (timeout guard)
TestNewErrorDoesNotMutateInput NewError copies variadics, no mutation
TestNewNetworkErrorValid valid HTTP code + inner wrap
TestNewNetworkErrorInvalidCode invalid code → Alert/1DJ1A2/500
TestNewNetworkErrorInvalidCodePreservesInnerWrap invalid code keeps the caller's inner
TestNewNetworkErrorInvalidCodeNoWrap invalid code, no inner supplied
TestNewNetworkErrorValidBlankWrap blank variadic is wrapped (not dropped) — consistent with NewError
TestUnwrapNilWhenNotWrapped Unwrap() is nil for a leaf
TestUnwrapReturnsInnerValue Unwrap() returns a Typ value (not *Typ)
TestErrorsAsTypValueTarget errors.As(&Typ) finds the outermost
TestErrorsAsTypPtrTarget errors.As(&*Typ) matches nothing (homogeneous chain)
TestErrorsIsTypValueTarget errors.Is by value matches any node
TestErrorsIsTypPtrTarget errors.Is by pointer matches nothing (no *Typ nodes)
TestErrorsIsUnwrapDepth full chain exposed via errors.Unwrap (all Typ values)
TestWrapTypInsideFmtError the headline use case: fmt.Errorf("%w") + errors.As/errors.Is
TestTypSatisfiesErrorInterface Typ satisfies error

Documentation

Index

Constants

View Source
const BlankErrorCode = "000000"

Variables

This section is empty.

Functions

This section is empty.

Types

type Level

type Level int
const (
	Panic   Level = 10 // For a FATAL ERROR which could or could not be recovered from
	Alert   Level = 8  // For a recoverable error that must be alerted (via email, webhook etc. / to be setup separately)
	Err     Level = 6  // For a recoverable error - something unexpected but not fatal
	Warning Level = 4  // For something that is not an error but MUST BE LOOKED INTO
	Notice  Level = 3  // For something that SHOULD be improvd
	Info    Level = 2  // For something that is rather just an information
	Unknown Level = 0  // Unknown level
	Debug   Level = -4 // Debug level
)

func FromShortString

func FromShortString(shortString string) Level

func (Level) Int

func (el Level) Int() int

func (Level) Int8

func (el Level) Int8() int8

func (Level) ShortStr

func (el Level) ShortStr() string

func (Level) String

func (el Level) String() string

type Typ

type Typ struct {
	Level            Level
	Code             string // LMID
	Message          string // The actual error message
	HttpResponseCode int    // In case we are trying to use this type for returning a network error
	DevMsg           string // Message meant for developers only (usually makes sense against a network error)
	WrappedError     *Typ   // Any wrapped errors that we want to embed in this error
	ExtraData        string // When we need to pass more values (errors are values (as said by Rob Pike))
}

func BlankError

func BlankError() Typ

func NewError

func NewError(errLevel Level, code string, msg string, wrappedError ...Typ) Typ

func NewNetworkError

func NewNetworkError(httpResponseCode int, errLvl Level, code string, msg string, devmsg string, wrappedError ...Typ) Typ

func (Typ) Error

func (e Typ) Error() string

func (Typ) IsBlank

func (e Typ) IsBlank() bool

func (Typ) IsBlankNetworkError

func (e Typ) IsBlankNetworkError() bool

func (Typ) IsNotBlank

func (e Typ) IsNotBlank() bool

func (Typ) IsNotBlankNetworkError

func (e Typ) IsNotBlankNetworkError() bool

func (Typ) String

func (e Typ) String() string

func (Typ) Unwrap added in v0.5.0

func (e Typ) Unwrap() error

Unwrap exposes the innermost wrapped error so that this type participates fully in the standard library's error-wrapping machinery (errors.Is, errors.As, errors.Unwrap) and with the "%w" verb of fmt.Errorf.

Returning nil when nothing is wrapped marks the end of the chain, which lets errors.* walk the entire WrappedError chain rather than stopping at the first appError value.

func (Typ) Wrap

func (e Typ) Wrap(wrappedErr Typ) Typ

func (Typ) WrapsErrorCode

func (e Typ) WrapsErrorCode(errCode string) bool

func (Typ) WrapsErrorLevel

func (e Typ) WrapsErrorLevel(errLvl Level, checkCurrErr bool) bool

Jump to

Keyboard shortcuts

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