惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Schneier on Security
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
T
Threat Research - Cisco Blogs
C
Cyber Attacks, Cyber Crime and Cyber Security
C
CXSECURITY Database RSS Feed - CXSecurity.com
A
Arctic Wolf
Security Latest
Security Latest
Simon Willison's Weblog
Simon Willison's Weblog
I
Intezer
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
T
Troy Hunt's Blog
Latest news
Latest news
Help Net Security
Help Net Security
S
Security Affairs
Webroot Blog
Webroot Blog
The Hacker News
The Hacker News
AI
AI
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
Tor Project blog
Forbes - Security
Forbes - Security
Google DeepMind News
Google DeepMind News
AWS News Blog
AWS News Blog
Attack and Defense Labs
Attack and Defense Labs
P
Proofpoint News Feed
www.infosecurity-magazine.com
www.infosecurity-magazine.com
H
Help Net Security
L
Lohrmann on Cybersecurity
S
SegmentFault 最新的问题
Google Online Security Blog
Google Online Security Blog
MongoDB | Blog
MongoDB | Blog
Cyberwarzone
Cyberwarzone
The Last Watchdog
The Last Watchdog
S
Securelist
N
News and Events Feed by Topic
S
Secure Thoughts
F
Fortinet All Blogs
博客园_首页
C
Cybersecurity and Infrastructure Security Agency CISA
量子位
M
MIT News - Artificial intelligence
F
Full Disclosure
T
The Blog of Author Tim Ferriss
T
Tailwind CSS Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Microsoft Security Blog
Microsoft Security Blog
I
InfoQ
P
Privacy International News Feed
L
LangChain Blog
Know Your Adversary
Know Your Adversary
C
CERT Recently Published Vulnerability Notes

Hacker News: Show HN

PurrrrrFocus: Pomodoro Timer App - App Store Workflow Engine — Multi-Step Orchestration for Bun RapidPhoto: Pro Photo Editor App - App Store GitHub - think41/extrasuite: Token-efficient pull/edit/push workflow for AI agents editing Google Workspace files (Sheets, Docs, Slides, Forms) GitHub - DheerG/swarms: Achieve extraordinary results with claude code across a variety of tasks SPICE simulation → oscilloscope → verification with Claude Code — Lucas Gerads Show HN: VCoding – A 5 MB native Windows IDE with no dynamic dependencies Show HN: LLMs don't hallucinate because they're bad at math, it's the format GitHub - Agent-FM/agentfm-core: AgentFM is a peer-to-peer network that turns everyday computers into a decentralized AI supercomputer. AgentFM lets you run massive AI workloads directly across a global mesh of idle CPUs and GPUs. Show HN: Tracking Top US Science Olympiad Alumni over Last 25 Years GitHub - Potarix/agent-hub: One place to talk to all your agents Show HN: Runtime security for AI agents(injection,tool abuse, data exfiltration) GitHub - dubeyKartikay/lazyspotify: Terminal Spotify client for macOS and Linux GitHub - the-banana-tool/king-louie: Easy to use GUI Personal AI Assistant. Win/Linux/Mac. Show HN I made my vacation rental bookable by AI agents–no Airbnb, 0% commission GitHub - basteez/jsf-autoreload: maven plugin to enable hot reload on jsf projects uvm32/hosts/host-gdbstub at main · ringtailsoftware/uvm32 GitHub - labsai/EDDI: Config-driven engine that turns JSON into production-grade AI agents. Multi-agent orchestration, 12+ LLM providers, MCP/A2A protocols, RAG, persistent memory, and enterprise compliance (EU AI Act, GDPR, HIPAA). Built on Quarkus. GitHub - glitchnsec/fortyone-oss: AI Executive Assistant Platform Quickstart | Alien GitHub - muxshed/shed: One stream in, or many. Every destination, simultaneously. No cloud middleman, no per-channel fees, no limits. GitHub - ocrbase-hq/ocrbase: 📄 PDF/IMG ->.MD/JSON Document OCR API for PaddleOCR and GLMOCR. Self-hostable. GitHub - impactjo/home-memory: MCP server that lets your AI assistant remember everything about your home. GitHub - Sets88/dbcls: DbCls is a powerful terminal database client that supports various databases GitHub - neptun2000/heor-agent-mcp GitHub - SeanFDZ/macmind: Single-layer transformer in HyperTalk for the classic Macintosh RollQuation: Math Puzzles - Apps on Google Play GitHub - dropbox/witchcraft Show HN: Agent-cache – Multi-tier LLM/tool/session caching for Valkey and Redis GitHub - opentalon/opentalon: OpenTalon is an open-source platform built from the ground up in Go as a robust alternative to OpenClaw LinkedIn™ 职位抓取工具 - Chrome 应用商店 GitHub - EdoardoBambini/Agent-Armor-Iaga: AI agents are getting tool access — shell, file system, databases, APIs, secrets. But **nobody is governing what they actually do with it**. Frameworks like LangChain, CrewAI, AutoGen, and Claude Code give agents the power to execute. Agent Armor gives you the power to control, audit, and approve every single action before it happens. HN Vibes — Week 15, Apr 7–13 2026 GitHub - chojs23/ec: Easy terminal-native 3-way git mergetool vim-like workflow GitHub - SethPyle376/hiraeth: Local AWS emulator focused on fast integration testing, with SQS support, SQLite-backed state, and a debug-friendly web UI. GitHub - JakOb-dotcom/cloud-sandbox-security-analysis: Technical analysis and Proof of Concept (PoC) regarding environment variable exfiltration in containerized cloud sandboxes via side-channel data leaks. Springboards - Flint Alpha Show HN: A simpler coding agent harness GitHub - audiodude/sudomake-friends GitHub - 256thFission/mini-mythos: OSS clone of Anthropic’s Mythos harness to locate C/C++ memory vulnerabilities Show HN: OpenParallax: OS-level privilege separation for AI agent execution Hacker News Sorted - Chrome 应用商店 Show HN: How to Install Docker on Ubuntu 24.04 LTS: Complete 2026 Guide GitHub - himanshudongre/smriti GitHub - sverrirsig/claude-control: macOS desktop dashboard for monitoring and managing multiple Claude Code sessions GitHub - ory/dockertest: Write better integration tests! Dockertest helps you boot up ephermal docker images for your Go tests with minimal work. Chiral - Chrome 应用商店 Show HN: Two Claudes collaborating through shared memory on a $100 mini-PC GitHub - pmichaillat/latex-cv: Minimalist LaTeX template for academic CVs GitHub - oguzbilgic/posse: A web UI for Anthropic Managed Agents. GitHub - sshiraz/depsly: Dependency risk analysis tool for npm packages ABI Add safari/agent-harness — Safari browser automation via safari-mcp by achiya-automation · Pull Request #212 · HKUDS/CLI-Anything GitHub - Halfblood-Prince/trustcheck: Verify PyPI package attestations and improve Python supply-chain security GitHub - oguzbilgic/kern-ai: Agents that do the work and show it. GitHub - bruits/satteri: High-performance Markdown and MDX processing for the JavaScript ecosystem GitHub - tylergibbs1/feedstock: High-performance web crawler and scraper for TypeScript, powered by Bun and Playwright GitHub - Grimm67123/grimmbot: The self-improving sandboxed and open-source AI agent. With persistent memory and scheduling. GitHub - whitevanillaskies/whitebloom: Local whiteboard that blooms. GitHub - hwdsl2/docker-whisper: Docker image for a self-hosted Whisper speech-to-text server with speaker diarization and OpenAI-compatible transcription and translation APIs. Powered by faster-whisper. Supports all Whisper models, NVIDIA GPU (CUDA) acceleration, JSON/SRT/VTT output, SSE streaming, offline mode, and multi-arch (amd64, arm64). GitHub - yisding/reviewwiggum GitHub - soatok/age-php GitHub - Luthiraa/markitme GitHub - stagas/rtdiff: realtime git diff gui and AI-assisted commits GitHub - tombedor/excalicharts GitHub - wh1le/excalidraw-edit: Open and edit .excalidraw files from the terminal. Offline, auto-saves to disk. MalExt Sentry - Malicious Extension Scanner - Chrome 应用商店 GitHub - syi0808/asciianimesvg: Generate animated ASCII art SVGs from text. CLI, Rust library, WASM, and web editor. GitHub - zaina-ml/ml_forge: A visual-based graph node editor for training computer vision models. GitHub - anakin87/llm-rl-environments-lil-course: 🌱 A little course on Reinforcement Learning Environments for evaluating and training Language Models GitHub - takaakit/superpowers-uml: Superpowers-UML modifies Superpowers to ensure a software development workflow in which AI agents design through UML modeling. AdriByte Studio - Sviluppo Web e Soluzioni Digitali GitHub - chouligi/angel-copilot: Your personalized Angel Investment Advisor Show HN: MoodSense AI (ML and FastAPI and Gradio, Deployed on Hugging Face) Moodsense Ai - a Hugging Face Space by aman179102 GitHub - agenteractai/lodmem: Level Of Detail Context Management for Agents GitHub - ostefani/subnetlens: A fast, concurrent network scanner with a TUI and plain-text CLI, built in Go. It discovers live hosts on your network, scans their open ports, resolves hostnames, and fingerprints operating systems—delivered. Cyber Pulse: Agentic Intel - Apps on Google Play Whisper API: Self-Hostable Speech to Text Transcription The Agent-Web Protocol Stack: A Research Thesis GitHub - msmarkgu/RelayFreeLLM: A restful API designed to route user prompts to various AI model providers. Show HN: Provepy – A Python decorator that proves your code using Lean and LLMs Show HN: Pardonned.com – A searchable database of US Pardons GitHub - patrickdappollonio/dux: Dux is a terminal UI that lets you run multiple AI coding agents side by side, each in its own git worktree, with full companion terminals, macros, commit generation, and a command palette that knows more tricks than you do. kMC Crystal Simulator Show HN: HyperFlow – A self-improving agent framework built on LangGraph GitHub - stef41/vibescore: 🎵 Grade your vibe-coded project. One command, instant letter grade across security, quality, dependencies, and testing. GitHub - stef41/lmscan: 🔍 Detect AI-generated text and fingerprint which LLM wrote it. Open-source GPTZero alternative. Zero dependencies, works offline. imgur.com GitHub - visionscaper/collabmem: Enabling long-term collaboration with Agentic AI - building up episodic and world model memory over time with in-context awareness 在 Steam 上购买 FriedrichAI: Offline AI 立省 10% GitHub - atripati/ark: AI Runtime Kernel — a context operating system for AI agents. Eliminates tool bloat, loads only what’s needed, and gives LLMs their reasoning space back. GitHub - nowork-studio/toprank: Open-source Claude Code skills for SEO, SEM, Google Ads GitHub - tacomanator/sash: Lightweight macOS menu bar app for reliably cycling through windows of the current application. Appents | Social Media Management for Product-First Teams GitHub - pnhoang/youtube-spam-blocker: Automatically detects and hides spam messages in YouTube Live chat. Set rate limits, keyword filters, and block repeat offenders. GitHub - decisionnode/DecisionNode: CLI + Local MCP - A shared structured memory store across Claude Code, Cursor, Windsurf, Antigravity, and every MCP client. Semantically queryable. GitHub - AvaCodeSolutions/django-email-learning: An open source Django app for creating email-based learning platforms with IMAP integration and React frontend components. The $100K Gap in Kubernetes Security Tooling Function Calling Harness: From 6.75% to 100%
GitHub - MarwanAlsoltany/serrors: Structured errors for Go: sentinel hierarchies, typed data, custom formatting, and slog integration.
2026-04-11 · via Hacker News: Show HN

CI Go Reference Go Report Card Coverage License: Apache 2.0

Go's standard error handling is minimal by design. It provides errors.New, fmt.Errorf, errors.Unwrap, errors.Join, errors.Is, errors.As, and then gets out of the way. For small programs, that is exactly right. For larger systems built from multiple layers and packages, the minimal approach leaves much to be desired and several practical problems unsolved.

The key insight the standard library encodes — and that many codebases fail to act on — is that errors are values. Unlike exceptions, they are not just signals that "something went wrong"; they carry meaning, context, and identity that can be inspected, matched, and acted upon programmatically. fmt.Errorf with %w gets partway there, but it stops at wrapping: the resulting value carries the chain but exposes none of the structure. serrors exists to fill that gap. It takes that idea to its logical conclusion: errors are first-class, typed, structured values with an identity, an operation chain, attached data, and a formatting contract.

The name follows the same convention as slog to log: the s stands for structured. Just as slog brought key-value context to logging, serrors brings structure, hierarchy, custom formatting, and inspection to errors without breaking compatibility with the standard library or introducing foreign concepts.

serrors is opinionated. It makes deliberate choices about error message format, delimiter conventions, and the shape of the public API. If any of those choices fit your codebase well, serrors will be a natural starting point. If they conflict with existing conventions, the formatting pipeline is customizable enough to accommodate most requirements, and the library is small enough to fork or vendor if deeper changes are needed. This is the kind of library you'll know when you need it, it is not meant for every use case.

The Problem

1. Errors lose context as they travel up the stack

A common Go pattern is:

if err != nil {
    return fmt.Errorf("database connect: %w", err)
}

This works, but there is no standard way to extract "database connect" programmatically. If the failed operation needs to be logged or reacted upon, the code must parse the error string. If tests or middleware need to match on the operation, they cannot do so reliably.

2. Flat sentinel errors do not compose

The standard approach to sentinel errors is:

var ErrNotFound         = errors.New("not found")
var ErrUserNotFound     = errors.New("user not found")
var ErrPermissionDenied = errors.New("permission denied")

These are flat, independent values. There is no way to express that ErrUserNotFound is a specialization of ErrNotFound. A caller that wants to handle all "not found" variants must enumerate every sentinel explicitly. Adding a new one requires updating every errors.Is check that should cover it, a maintenance problem that grows with the error tree and application complexity.

3. There is no standard format for error messages

fmt.Errorf("%w: %s", ErrBase, "detail") is the de-facto way to attach detail to a sentinel at runtime. It returns a *fmt.wrapError, not a structured type. The standard library imposes no structure on the resulting string, so every package invents its own conventions. When the underlying error already contains a label prefix, wrapping it again produces "app: op: app: nested op: cause"; a doubled label with no way to strip it. There is also no way to register a custom formatter for a third-party error type that appears somewhere deep in the chain.

4. Structured data cannot be attached to an error without a custom type

Errors that carry context (a request ID, a host name, a retry count) have no standard home for that data. The options are to embed the values in the error string (losing machine-readability), or to define a bespoke error struct for every context type (boilerplate at every layer of the stack). Neither composes well across package boundaries.

5. There is no call-site location without a debugger

By the time an error reaches a log statement or a top-level handler, there is no record of which file and line it came from. The only options are to print a full stack trace on every error (noise) or to manually attach caller information (boilerplate that is easy to forget).

6. Logging a rich error requires inspection boilerplate

There is no standard way to pass a structured error to slog and get key-value output. The typical pattern is a chain of errors.As calls that extract fields and build attributes manually; duplicating structure that is already inside the error and coupling the logging code to the error's internals.

The Solution

serrors solves these problems with a small set of composable ideas:

  1. Domain isolation: a Domain owns a label and formatting chain and produces all its *Error values. Two concerns using serrors share no state unless they explicitly share a Domain instance.
  2. Structured errors: Error carries an Op string (the operation/operator/component name), an underlying Err, and an optional Data field for structured context. Data is invisible to Error() but fully accessible for logging and programmatic inspection.
  3. Sentinel hierarchy API (Sentinel, Derive, Detail, Detailf): replaces flat errors.New and fmt.Errorf wrappers for static package-level error variables, giving every node a parent it automatically matches via errors.Is.
  4. Three-layer formatting pipeline (FormatFunc, Delimiters, Formatter): controls how individual leaf errors render, how ops and labels are joined, and how the final string is assembled; each layer is independently customizable per domain.
  5. Opt-in stack traces via CaptureStackTrace(), passed as the data argument to WrapWith. No global flag; a trace is captured only where explicitly requested, stored as a typed StackTrace in Error.Data like any other context.
  6. Native slog integration: *Error implements slog.LogValuer. Passing an error to any slog call automatically emits its message and all structured Data as grouped attributes, no manual extraction needed.

Everything produced by serrors is compatible with the standard library: errors.Is, errors.As (and errors.AsType[T]), and errors.Unwrap all work as expected.

API Design Philosophy

serrors keeps its API surface small, built around a few orthogonal primitives rather than a wide family of convenience wrappers.

Many error libraries grow by layering With* helpers for every wrapping variation, which fragments the API and increases cognitive load. serrors instead provides Domain.Sentinel, Domain.Wrap, Domain.WrapWith, Error.Derive, Error.Detail, and domain-level configuration; primitives expressive enough to cover common use cases without narrowly scoped helpers for every variation.

This keeps the package lightweight:

  • Conceptually: fewer entry points, consistent mental model.
  • Practically: less API surface to learn, document, and maintain.
  • Operationally: no hidden behavior, no global switches, no implicit overhead.

When additional control is needed, it comes through explicit mechanisms (structured Data, domain-scoped formatters, and delimiter configuration) not by expanding the function set.

Compared to Existing Packages

serrors sits in the same space as github.com/pkg/errors, go.uber.org/multierr, github.com/cockroachdb/errors, and the Kubernetes error helpers, but consolidates their most useful ideas into a single domain-scoped model: hierarchical sentinels, structured data, opt-in stack traces, customizable formatting, and standard library interoperability.

Unlike those libraries, serrors is deliberately the lightest: no global switches, no mandatory stack-trace overhead, no hidden framework behavior. Errors are created and extended explicitly, no implicit annotation, automatic wrapping, or background propagation of context.

serrors does not replace the standard library or impose a new error model. It extends the existing one, remaining fully compatible with error, fmt.Errorf, errors.Is, and errors.As. It can be introduced incrementally: used where structure and inspection are needed, and set aside where they are not. Every *Error value produced by serrors:

  • Satisfies the error interface.
  • Works with errors.Is and errors.As* out of the box.
  • Chains correctly with errors.Unwrap.
  • Composes with errors.Join results.

Installation

go get github.com/MarwanAlsoltany/serrors

Examples

Runnable, self-verifying examples covering all major patterns are in example_test.go. They are compiled and executed on every go test ./... run and are rendered automatically on pkg.go.dev alongside the API documentation.

Quick Start

This package supports two modes of operation: package-level functions on a default domain (suitable for most small applications), and explicit Domain instances for library authors or multi-domain applications that need isolated error namespaces.

Example (package-level, using the default domain):

import (
    "database/sql"

    _ "github.com/lib/pq"
    "github.com/MarwanAlsoltany/serrors"
)

func openDB(dsn string) error {
    db, err := sql.Open("postgres", dsn)
    if err != nil {
        // wrap a low-level error with operation context
        return serrors.Wrap("database open", err)
    }
    return db.Ping()
}

The error message will be:

error: database open: dial tcp: connection refused

The label ("error") is the default. Can be freely changed to match some package:

Note: The default label "error" is deliberately non-idiomatic. It is meant to stand out and prompt replacement with a meaningful name.

func init() { serrors.SetDefault(serrors.New("my-app")) }
// or omitted entirely with serrors.New("")

Now the same error reads:

my-app: database open: dial tcp: connection refused

Sentinel Hierarchies

This is where serrors diverges most visibly from the standard library to provide a better experience for static sentinels.

With the standard library, sentinels are typically defined as var ErrX = errors.New("x"), which means they are flat and cannot express parent-child relationships.

A typical package declares its error tree once, at the package level, and everything else uses those sentinels. With serrors the whole tree is composed of *Error values (satisfying error interface), which means every node is inspectable and participates in errors.Is traversal automatically. Making it possible to catch not only operation failures but complete component/subsystem failure by matching on a root sentinel.

var domain = serrors.New("my-app")

// top-level sentinels: domain.Sentinel() creates an *Error with no parent
// which serves as a root for its subtree
var (
    ErrAuth     = domain.Sentinel("auth")
    ErrNetwork  = domain.Sentinel("network")
    ErrDatabase = domain.Sentinel("database")
)

// child sentinels: Err*.Derive() creates an *Error that wraps its parent (top-level sentinel)
var (
    ErrAuthExpired = ErrAuth.Derive("token expired")
    ErrAuthInvalid = ErrAuth.Derive("token invalid")

    ErrNetworkDial    = ErrNetwork.Derive("dial")
    ErrNetworkTLSHand = ErrNetwork.Derive("tls handshake")

    ErrDatabaseConnect = ErrDatabase.Derive("connect")
    ErrDatabaseTimeout = ErrDatabase.Derive("timeout")
    ErrDatabaseQuery   = ErrDatabase.Derive("query")
)

// leaf detail: .Detail() and .Detailf() wrap an *Error with a plain message string,
// useful when a specific error string is needed but not a named sentinel
return ErrNetworkDial.Detail("connection refused")
return ErrDatabaseConnect.Detailf("timed out after %ds", int(timeout/time.Second))

Every node in this tree automatically satisfies:

errors.Is(ErrDatabaseTimeout, ErrDatabase) // true
errors.Is(ErrDatabaseConnect, ErrDatabase) // true
errors.Is(ErrDatabaseQuery, ErrDatabase) // true
errors.Is(ErrDatabase, domain.Root()) // true

The full hierarchy is provided for free, with no need for custom Is methods or manual wrapping.

Derive vs. Detail/Detailf

Derive(op) Detail(msg) / Detailf(fmt, args...)
Return value *Error error
errors.Is inspectable yes yes
errors.As inspectable yes, Op field yes, parent *Error.Op only (detail text is plain string)
Wrapping with %w no Detailf only
Participates in formatters yes no (plain fmt.Errorf wrap)
Best used for named sentinels in error tree one-off detail on a leaf

The error return type for Detail* is intentional: the message does not belong on *Error.Op, which is structurally an operation name, not a message string. Detailf also delegates to fmt.Errorf internally, making *Error impossible to return without reimplementing %w semantics. See the Detail source doc comment for the full rationale.

Detailf supports %w, so any wrapped error remains reachable via errors.Is and errors.As:

err := ErrService.Detailf("query failed: %w", pkgErr)
// errors.Is(err, pkgErr) == true

The rule of thumb: if the error appears as a package-level var, use Error.Derive. For adding a runtime detail to an existing sentinel at the call-site, use Error.Detail*, or Domain.Wrap*.

Wrapping Runtime Errors

For errors that are not static sentinels but carry runtime context, use Wrap and Wrapf:

func (s *Store) QueryUser(id string) (*User, error) {
    row := s.db.QueryRow("SELECT * FROM users WHERE id = $1", id)
    if err := row.Scan(...); err != nil {
        // wrap the database error under the "query user" operation
        return nil, domain.Wrap("query user", err)
    }
    return user, nil
}

func (s *Store) CreateUser(u *User) error {
    if err := validate(u); err != nil {
        // formatted detail message
        return domain.Wrapf("create user", "validation failed for email %q: %w", u.Email, err)
    }
    // ...
}

Wrap also supports multiple errors in a single call (delegates to errors.Join internally):

var errs []error
for _, item := range batch {
    if err := process(item); err != nil {
        errs = append(errs, err)
    }
}
// returns nil if errs is empty or all-nil, no guard needed
return domain.Wrap("process batch", errs...)

The output for a multi-error result is a compact single-line (unlike the default multi-line errors.Join format):

The ":" and ";" delimiters are configurable via WithDelimiters.

my-app: process batch: item 2: invalid; item 5: not found; item 9: timed out

Attaching Structured Data

Use WrapWith and WrapWithf to attach arbitrary structured context to an error without embedding it in the error string. The data is stored in Error.Data and is retrievable programmatically, but is intentionally excluded from Error.Error(), keeping messages clean while still providing rich context for logging and inspection.

type ConnCtx struct {
    Host    string
    Attempt int
}

// optional: implement slog.LogValuer for automatic slog expansion
func (c ConnCtx) LogValue() slog.Value {
    return slog.GroupValue(
        slog.String("host", c.Host),
        slog.Int("attempt", c.Attempt),
    )
}

func dial(host string, attempt int) error {
    conn, err := net.Dial("tcp", host)
    if err != nil {
        return domain.WrapWith("dial", ConnCtx{host, attempt}, err)
    }
    return nil
}

Raw []slog.Attr slices are also accepted directly:

return domain.WrapWith("dial",
    []slog.Attr{slog.String("host", host), slog.Int("attempt", n)},
    err,
)

See the next section for how to retrieve and log the attached data.

Inspecting Errors

All standard library inspection tools work unchanged:

// check for a sentinel (works at any depth in the chain)
if errors.Is(err, ErrDatabase) {
    log.Warn("database error", "err", err)
}

// extract the operation name
var opErr *serrors.Error
if errors.As(err, &opErr) {
    metrics.Increment("errors", "op", opErr.Op)
}

// check for a concrete underlying type
if pkgErr := errors.AsType[*pkg.Error](err); pkgErr != nil {
    if pkgErr.Code == "123" {
        return ErrSomeError
    }
}

Because *Error implements Unwrap(), the entire standard library chain-walking machinery traverses it correctly and as one would expect.

serrors also provides a set of one-liner helpers that avoid the boilerplate of declaring a temporary variable and calling errors.As manually:

// extract the first *Error in the chain without a temporary variable
if e, ok := serrors.AsError(err); ok {
    metrics.Increment("errors", "op", e.Op)
    // e.Domain is also accessible, the Domain that produced the error
}

// first non-nil Data of the given type in the chain
ctx, ok := serrors.AnyDataAs[ConnCtx](err)

// all non-nil Data values of the given type in the chain, outermost first
ctxs := serrors.AllDataAs[any](err)

// walk the full chain
serrors.Walk(err, func(e *serrors.Error) bool {
    fmt.Println(e.Op, e.Data)
    return true
})

// check whether an error belongs to a specific domain
if domain.Contains(err) {
    // equivalent to errors.Is(err, domain.Root())
}

Accessing Structured Data

Error.Data is the structured context attached via WrapWith/WrapWithf. It is not part of the error string, so it must be retrieved programmatically.

Direct type assertion, when only one layer of context is expected:

var opErr *serrors.Error
if errors.As(err, &opErr) && opErr.Data != nil {
    if errCtx, ok := opErr.Data.(ConnCtx); ok {
        log.Printf("failed on host %q (attempt=%d)", errCtx.Host, errCtx.Attempt)
    }
}

AllDataAs: collects typed Data from every *Error node in the chain, outermost first. Useful when multiple wrapping layers each attach context of the same type:

for _, ctx := range serrors.AllDataAs[ConnCtx](err) {
    fmt.Printf("context: %+v\n", ctx)
}
// use serrors.AllDataAs[any](err) to collect all Data values regardless of type

LogAttrs: the slog-oriented counterpart to AllDataAs. Walks the chain and converts each Data value to []slog.Attr according to these rules:

  • []slog.Attr: returned verbatim.
  • slog.LogValuer: that resolves to a group, the group's attributes are inlined.
  • Any other type: wrapped as a single slog.Any(...) attribute under the key DataKey ("data").
slog.LogAttrs(ctx, slog.LevelError, "dial failed",
    append(serrors.LogAttrs(err), slog.String("err", err.Error()))...,
)
// -> level=ERROR msg="dial failed" host=db attempt=2 err="service: dial: connection refused"

*Error also implements slog.LogValuer directly, so passing it as an slog attribute value is idiomatic and produces the same structured output when Data is present; no explicit LogAttrs call needed:

// no Data anywhere in chain -> plain string value
slog.Error("dial failed", "err", wrappedErr)
// -> err="service: dial: connection refused"

// with Data -> slog group nested under the key
slog.Error("dial failed", "err", wrappedErr)
// -> err.message="service: dial: connection refused" err.host=db err.attempt=2

Stack Traces

Stack traces are opt-in by design. There is no global flag to activate; a trace is captured only where the consumer explicitly asks for it by passing CaptureStackTrace() as the data argument to WrapWith:

err := domain.WrapWith("op", serrors.CaptureStackTrace(), cause)

The returned StackTrace is stored in Error.Data like any other structured context. The standard inspection API retrieves it, no special functions are needed:

stacks := serrors.AllDataAs[serrors.StackTrace](err)
for _, st := range stacks {
    for _, f := range st.Frames() {
        fmt.Printf("%s:%d %s\n", f.File, f.Line, f.Function)
    }
}

Depth/Skip Control for Helper Wrappers

When CaptureStackTrace is called inside a helper function rather than at the actual error site, the helper's frame appears at the top of the trace. Use CaptureStackTraceN(depth, skip) and pass 1 for each wrapper layer between the call site and CaptureStackTraceN:

func wrapWithTrace(op string, cause error) error {
    // skip=1 removes wrapWithTrace from the top of the stack trace
    return domain.WrapWith(op, serrors.CaptureStackTraceN(serrors.DefaultStackDepth, 1), cause)
}

For the common case (i.e. default depth, direct call) CaptureStackTrace() is sufficient (captures 32 frames).

Slog Integration

StackTrace implements slog.LogValuer as well. When it is stored as Error.Data, LogAttrs and Error.LogValue automatically expand it into a single semicolon-separated string attribute under the key StackTraceKey ("stack"):

slog.Error("request failed", "err", wrappedErr)
// -> err.message="service: op: cause" err.stack="github.com/user/app/store.go:42 main.openDB; github.com/user/app/main.go:17 main.run"

The single-line format is intentional, human-readable, searchable, and free of multi-line stack-dump noise.

Note: Build-environment prefixes are stripped from file paths: module cache -> module@version/file.go, main module -> module/file.go, stdlib -> package/file.go.

Domain Isolation

Each Domain has its own label and formatting chain, fully isolated from every other domain. Two packages that both depend on serrors share no state:

// in package A:
var domainA = serrors.New("package-a")

serrors.RegisterTypedFormatFunc(domainA, func(e *MyErrorA) string {
    return fmt.Sprintf("A: %s", e.Detail)
})

// in package B:
var domainB = serrors.New("package-b")

serrors.RegisterTypedFormatFunc(domainB, func(e *MyErrorB) string {
    return fmt.Sprintf("B: %s", e.Info)
})

Package A's format function never sees Package B's errors and vice versa. The application that imports both can add its own format functions to either domain or to the default one.

A Complete Library Setup

package library

import (
    "github.com/MarwanAlsoltany/serrors"
)

var domain = serrors.New("my-library")

// exported root for downstream errors.Is checks
var ErrLibrary = domain.Root()

// public error hierarchy
var (
    ErrConfig        = domain.Sentinel("config")
    ErrConfigMissing = ErrConfig.Derive("missing")
    ErrConfigInvalid = ErrConfig.Derive("invalid")

    ErrIO         = domain.Sentinel("io")
    ErrIORead     = ErrIO.Derive("read")
    ErrIOWrite    = ErrIO.Derive("write")
    ErrIONotFound = ErrIO.Derive("not found")
)

func init() {
    // register a format function for a third-party
    // error type specific to this library
    serrors.RegisterTypedFormatFunc(domain, func(e *os.PathError) string {
        return fmt.Sprintf("path %q: %s", e.Path, e.Err)
    })
}

Consumers of library can check:

errors.Is(err, library.ErrConfigMissing) // true; also matches ErrConfig and ErrLibrary

Copying and Extending Domains

Two methods create a new Domain derived from an existing one, plus one composable Option. They differ in whether a root link is established:

With(opts...) Sub(label, opts...)
Copies label + config yes yes
Applies new options yes yes
Root relationship independent (no is-a link) linked (is-a: child root wraps parent root)
errors.Is(childErr, parent.Root()) false true
Best used for snapshots, reconfigured sibling domain sub-component inheriting error identity

This allows for creating intricately structured domain hierarchies with shared or independent configuration as needed.

With

Returns a copy of the domain with its own independent root sentinel and any provided options applied. Changes to the child (formatters, etc.) do not affect the original. Calling With with no options is equivalent to a full clone.

// exact copy, independent root
copy := domain.With()

// peer with a different label but same formatting config
peer := domain.With(serrors.WithLabel("other-service"))

Sub

Creates a sub-domain (child) whose root sentinel wraps the parent's root, establishing an is-a relationship. Errors produced by the child automatically satisfy errors.Is(err, parent.Root()). The child inherits the parent's (base's) config (format functions, delimiters, formatter); its label is set by the required label argument, not inherited.

var main = serrors.New("main", serrors.WithFormatFunc(sharedFormatter))
var sub = main.Sub("sub-component")

err := sub.Wrap("op", errors.New("x"))
errors.Is(err, sub.Root())  // true
errors.Is(err, main.Root()) // true   <- is-a link

WithBase

The composable option counterpart to Sub. Use it with New or With when the label is set separately or when combining multiple options:

// same effect as main.Sub("sub-component")
sub := serrors.New("sub-component", serrors.WithBase(main))

// start fresh (no config inheritance) with an is-a link and custom delimiters
sub := serrors.New("sub-component", serrors.WithBase(main), serrors.WithDelimiters(custom))

Unlike Sub, WithBase does not set a label; the label is whatever the domain already has or is set explicitly by WithLabel.

Error Formatting

serrors produces flat error messages that are a blend of idiomatic Go style and opinionated single-line structure. The messages are friendly to structured log systems, easy to grep, and avoid the multi-line stack-trace noise that plagues some error libraries.

The formatting pipeline has three independent customization points, each operating at a different layer:

Layer API Controls
Leaf text FormatFunc (via WithFormatFunc, RegisterFormatFunc, RegisterTypedFormatFunc) How individual non-*Error errors (i.e. non-serrors errors) are rendered into text (i.e. final error string)
Delimiters Delimiters (via WithDelimiters, With*Delimiter) The delimiter strings that join label, ops, tail, and joined errors
Full assembly Formatter (via WithFormatter) The complete output; receives raw structured data and returns the final error string

Not every application needs all three layers. Start with the narrowest tool and only reach for the next when the previous is insufficient:

  1. FormatFunc is the right choice in most cases. It controls how individual non-*Error error types render their text without affecting message structure. This is all most applications ever need.
  2. Delimiters is the right choice when only the separator convention needs to change. It requires no function and has no runtime cost.
  3. Formatter is a last resort. Use it only when the delimiter-based assembly cannot produce the required format; for example, when output must follow an external schema.

To understand why both FormatFunc and Formatter exist, it helps to know how the default format is assembled. The formatter walks the *Error chain collecting ops, then reaches the innermost non-*Error error i.e. the leaf. The leaf is formatted into a string called the tail. The final output is the label, followed by the op chain, followed by the tail, each joined by delimiters:

<label><delimiter.label><op1><delimiter.part><opN>...<delimiter.part><tail>

Default Format

The default formatter assembles messages in this shape:

<label>: [<ancestor-op>: ...] <op>: <underlying>

When the domain label is empty, the label and its delimiter are omitted entirely:

[<ancestor-op>: ...] <op>: <underlying>

This mirrors how an empty Op is silently omitted from the chain. An empty label is valid and can be set via New("") or WithLabel("") when no prefix is desired.

The three delimiter strings used are:

Delimiter Default Role
Label ": " Between the domain label and the first op or message
Part ": " Between individual ops and between the last op and the tail
Join "; " Between children of a multi-error (errors.Join) aggregate

For a chain like ErrConfigMissing above:

library: config: missing

For a wrapped runtime error under ErrIORead:

library: io: read: open /etc/app.conf: no such file or directory

For multiple aggregated errors:

library: io: read: open /etc/a.conf: no such file or directory; open /etc/b.conf: permission denied

Format Functions

Format functions are consulted in reverse registration order; the last registered format function to return ok=true wins (last-match semantics), this allows overriding previous format functions. If no format function matches — including for foreign types (fmt.Errorf, errors.New, third-party errors) that may appear anywhere in the chain — the error's own Error() method is used as a fallback.

Three ways to register a FormatFunc are available, each suited to a different scenario:

At Construction Time (WithFormatFunc)

Usage: When the format function is known at domain setup time and should be part of the domain's permanent configuration. Construction-time format functions are inherited by Sub and With child domains and cannot be removed after the domain is created. Call WithFormatFunc multiple times to register multiple format functions.

domain := serrors.New("app",
    serrors.WithFormatFunc(func(err error) (string, bool) {
        var pkgErr *pkg.Error
        if !errors.As(err, &pkgErr) {
            return "", false
        }
        return fmt.Sprintf("pkg(%s): %s", pkgErr.Code, pkgErr.Message), true
    }),
)
// app: query: pkg(123): something went wrong

At Runtime - Manual (RegisterFormatFunc)

Usage: When the format function needs to be registered or unregistered dynamically after the domain is created, or when a single function handles multiple error types. Returns an unregister function so the format function can be unregistered cleanly (e.g. in tests).

unregister := domain.RegisterFormatFunc(func(err error) (string, bool) {
    pkgErr, ok := err.(*pkg.Error)
    if !ok {
        return "", false
    }
    return fmt.Sprintf("pkg-%s: %s", pkgErr.Code, pkgErr.Message), true
})
defer unregister()

Note: RegisterFormatFunc can be used to register a catch-all format function that handles multiple types in one place, but it requires manual type assertions and nil checks.

At Runtime - Type-Safe (RegisterTypedFormatFunc)

Usage: When only a single concrete error type needs a format function. The generic wrapper handles the type assertion and nil check automatically. The nil check matters here: in Go, a *pkg.Error nil stored in an error interface is not nil at the interface level, so a naive format function could panic. serrors detects and skips typed nils before calling the format function.

unregister := serrors.RegisterTypedFormatFunc(domain, func(err *pkg.Error) string {
    return fmt.Sprintf("pkg: %s (code=%s)", err.Message, err.Code)
})
defer unregister()

Customizing Delimiters

Usage: When the default ": " / "; " delimiters clash with surrounding conventions. For example, a log pipeline that already uses ": " as a field delimiter, or a display format that prefers arrow-separated chains.

Call WithDelimiters to replace all three delimiters at once:

domain := serrors.New("service", serrors.WithDelimiters(serrors.Delimiters{
    Label: " | ",
    Part:  " > ",
    Join:  " & ",
}))
// service | op1 > op2 > underlying
// service | op > err a & err b & err c   (multi-error)

Individual helpers are also available when only one field needs to change:

domain := serrors.New(
    "service",
    serrors.WithLabelDelimiter(" | "),
    serrors.WithPartDelimiter(" > "),
    serrors.WithJoinDelimiter(" & "),
)

Custom Formatter

Usage: When the delimiter-based assembly model is not expressive enough. For example, when the output format must follow an external schema, or when ops should be encoded differently from the tail (e.g. as a path prefix rather than a colon-separated chain).

Implement the Formatter interface (or use the FormatterFunc adapter) to take full control of the final string. The Format method receives the leaf error and a FormatSpec containing format specification data:

  • spec.Label: The domain label string.
  • spec.Ops: The ordered ops slice, ancestor-first.
  • spec.Delimiters: The domain's configured delimiters.
  • spec.Apply(err): Formats any error through the domain's FormatFunc chain, applying last-match semantics and falling back to err.Error(). Use this to format both the leaf error and any children of an errors.Join aggregate.
domain := serrors.New("app",
    serrors.WithFormatter(serrors.FormatterFunc(func(err error, spec serrors.FormatSpec) string {
        if len(spec.Ops) == 0 {
            return fmt.Sprintf("[%s] %s", spec.Label, spec.Apply(err))
        }
        return fmt.Sprintf("[%s/%s] %s", spec.Label, strings.Join(spec.Ops, "/"), spec.Apply(err))
    })))
// [app/op1/op2] underlying error message

Use DefaultFormatter() as a base when you only want to wrap or post-process the output rather than replacing it entirely:

base := serrors.DefaultFormatter()
domain := serrors.New("app",
    serrors.WithFormatter(serrors.FormatterFunc(func(err error, spec serrors.FormatSpec) string {
        return "[" + base.Format(err, spec) + "]"
    })))
// [app: op: underlying]

Using All Three Together

For the most control and to produce any output format, combine a FormatFunc, Delimiters, and a full Formatter. Each controls a distinct layer:

domain := serrors.New("app",
    // 1. leaf text: custom format for a third-party error type
    serrors.WithFormatFunc(func(err error) (string, bool) {
        var pkgErr *pkg.Error
        if !errors.As(err, &pkgErr) {
            return "", false
        }
        return fmt.Sprintf("pkg(%s): %s", pkgErr.Code, pkgErr.Message), true
    }),
    // 2. delimiters: non-default delimiters (applied inside spec.Apply(err))
    serrors.WithDelimiters(serrors.Delimiters{Label: " | ", Part: " > ", Join: " & "}),
    // 3. full assembly: custom top-level structure
    //    spec.Apply(err) returns "pkg(123): ..." with " & " for multi-errors
    serrors.WithFormatter(serrors.FormatterFunc(func(err error, spec serrors.FormatSpec) string {
        return fmt.Sprintf("[%s] %s: %s", spec.Label, strings.Join(spec.Ops, "/"), spec.Apply(err))
    })),
)
// [app] op1/op2: pkg(123): something went wrong

Concurrency

All Domain operations are safe for concurrent use with the following guidance:

  • Error formatting (err.Error(), e.LogValue()) is always safe, with no restrictions.
  • Register*FormatFunc is safe for concurrent use, including concurrent calls on the same domain. Calling it once at program startup (typically in init()) is conventional but not required.
  • Domain's Sentinel/Wrap*/WrapWith*, and Error's Derive/Detail*; are safe to call from any goroutine at any time.

API Reference

Package-Level (default domain)

// the default domain instance for package-level functions
Default() *Domain
// replace the default domain with a custom one
SetDefault(d *Domain)
// replace the default domain with a fresh stock domain (for tests only)
Reset()
// create a new named sentinel on the default domain
Sentinel(op string) *Error
// wrap one or more errors with an operation context
Wrap(op string, errs ...error) error
// wrap with formatted detail
Wrapf(op string, format string, args ...any) error
// wrap with structured data
WrapWith(op string, data any, errs ...error) error
// wrap with structured data and formatted detail
WrapWithf(op string, data any, format string, args ...any) error
// register a format function for the default domain
// see also RegisterTypedFormatFunc for a type-safe alternative
RegisterFormatFunc(fn FormatFunc) func()

Domain Methods

// create a new error domain with the given label and optional options
serrors.New(label string, opts ...Option) *Domain

// the root sentinel for this domain, parent of all sentinels of the domain
d.Root() *Error
// create a new sentinel error with the given operation name
d.Sentinel(op string) *Error
// get the label of this domain
d.Label() string
// reports whether err belongs to this domain (true when errors.Is(err, d.Root()))
d.Contains(err error) bool
// wrap one or more errors with an operation context
d.Wrap(op string, errs ...error) error
// wrap with formatted detail
d.Wrapf(op string, format string, args ...any) error
// wrap with structured data
d.WrapWith(op string, data any, errs ...error) error
// wrap with structured data and formatted detail
d.WrapWithf(op string, data any, format string, args ...any) error
// register a FormatFunc for this domain; returns an unregister function
d.RegisterFormatFunc(fn FormatFunc) func()
// create a new domain by copying the receiver and applying opts; independent root (no is-a link)
d.With(opts ...Option) *Domain
// create a sub domain with a new label that inherits the parent's root sentinel hierarchy (is-a link)
d.Sub(label string, opts ...Option) *Domain

Domain Options

// set the domain label (useful in With to rename without an is-a link)
WithLabel(label string) Option
// establish an is-a link to base: errors from this domain satisfy errors.Is against base.Root()
WithBase(base *Domain) Option
// set all three delimiter strings at once
WithDelimiters(d Delimiters) Option
// set only the label delimiter (between domain label and first op/tail)
WithLabelDelimiter(delim string) Option
// set only the part delimiter (between ops and tail in the default assembly)
WithPartDelimiter(delim string) Option
// set only the join delimiter (between errors.Join children)
WithJoinDelimiter(delim string) Option
// append a FormatFunc to the chain at construction time; can be called multiple times
WithFormatFunc(fn FormatFunc) Option
// set a full-control Formatter (replaces default delimiter-based assembly)
// use FormatterFunc to wrap a plain function
WithFormatter(f Formatter) Option

Error Type and Methods

Type:

type Error struct {
    Op     string   // operation/operator/component name
    Err    error    // underlying error
    Data   any      // optional structured context attached via WrapWith/WrapWithf; nil for sentinels and plain Wrap calls
    Domain *Domain  // domain this error belongs to; nil uses Default() at formatting/Is time (nil-means-default)
}

Methods:

// typed child sentinel, same domain
e.Derive(op string) *Error
// plain leaf with literal message
e.Detail(msg string) error
// plain leaf with formatted message; supports %w for inner error wrapping
e.Detailf(format string, args ...any) error
// implements error interface (Data is not included in the string representation)
e.Error() string
// single-error unwrap
e.Unwrap() error
// matches root sentinel of domain (and ancestor roots for Sub)
e.Is(target error) bool
// implements slog.LogValuer; returns a plain string when no Data is present,
// or a slog group (message + all Data attrs) when at least one node carries Data
e.LogValue() slog.Value

Standalone Functions

// top-level generic function for registering a typed format function on a specific domain
RegisterTypedFormatFunc[T error](d *Domain, fn func(T) string) func()

// walk chain calling fn for each *Error node, outermost first; return false from fn to stop early
Walk(err error, fn func(*Error) bool)
// extract the first *Error from the chain; ok=false if none found
AsError(err error) (*Error, bool)
// first Data value in the chain assignable to T; ok=false if none found
AnyDataAs[T any](err error) (T, bool)
// walk chain, collect Data values assignable to T, outermost first
AllDataAs[T any](err error) []T

// walk chain, collect Data as slog attrs, outermost first
LogAttrs(err error) []slog.Attr

// capture the current goroutine's call stack, up to DefaultStackDepth frames
CaptureStackTrace() StackTrace
// like CaptureStackTrace but with explicit frame limit and skip count
CaptureStackTraceN(depth, skip int) StackTrace

// return the built-in Formatter; useful as a base for wrapping the default output
DefaultFormatter() Formatter
// format err using d's pipeline; useful for testing custom formatting configurations
FormatError(d *Domain, err error) string

Types

// StackTrace holds raw program counters captured at a call site
type StackTrace []uintptr
// resolve the raw program counters to human-readable runtime.Frame values
st.Frames() []runtime.Frame
// implements slog.LogValuer; emits all frames as a semicolon-separated string
// under StackTraceKey ("stack") in a slog group
st.LogValue() slog.Value

// FormatFunc formats a single error to a compact string representation;
// return ("", false) when the format function does not handle the given type
type FormatFunc func(error) (s string, ok bool)

// Formatter is the full-control interface for assembling an error's string representation
type Formatter interface {
    Format(err error, spec FormatSpec) string
}
// FormatterFunc adapts a plain function to the Formatter interface
type FormatterFunc func(err error, spec FormatSpec) string

// FormatSpec carries the formatting pipeline for a domain snapshot
type FormatSpec struct {
    Label      string       // domain label, e.g. "my-app"; can be empty
    Ops        []string     // operation chain, ancestor-first
    Delimiters Delimiters   // the domain's configured delimiter strings
}

// Apply formats err through the domain's FormatFunc chain; falls back to err.Error()
(spec FormatSpec).Apply(err error) string

// Delimiters controls the three delimiter strings used when formatting errors
type Delimiters struct {
    Label string // between domain label and first op/tail; default ": "
    Part  string // between individual ops and between last op and tail; default ": "
    Join  string // between children of an errors.Join aggregate; default "; "
}

Constants

DefaultStackDepth = 32        // default frame limit used by CaptureStackTrace
DataKey           = "data"    // slog attribute key used when Data is wrapped as a fallback slog.Any attribute
MessageKey        = "message" // slog attribute key for the error message in Error.LogValue groups
StackTraceKey     = "stack"   // slog attribute key for the stack trace in StackTrace.LogValue groups

License

See LICENSE.