{"id":48581830,"url":"https://github.com/wood-jp/xerrors","last_synced_at":"2026-04-08T17:03:28.315Z","repository":{"id":339665117,"uuid":"1150028350","full_name":"wood-jp/xerrors","owner":"wood-jp","description":"Golang extended error functionality. Wrap any error with any data stucture using generics; automatically log that data, or extract directly it later. Loggable stacktraces out of the box.","archived":false,"fork":false,"pushed_at":"2026-03-24T17:58:49.000Z","size":64,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-24T19:05:11.454Z","etag":null,"topics":["error-handling","golang","slog","stacktrace"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/wood-jp.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":"CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-04T19:52:46.000Z","updated_at":"2026-03-24T17:58:52.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/wood-jp/xerrors","commit_stats":null,"previous_names":["wood-jp/xerrors"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/wood-jp/xerrors","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wood-jp%2Fxerrors","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wood-jp%2Fxerrors/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wood-jp%2Fxerrors/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wood-jp%2Fxerrors/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/wood-jp","download_url":"https://codeload.github.com/wood-jp/xerrors/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wood-jp%2Fxerrors/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31564918,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-08T14:31:17.711Z","status":"ssl_error","status_checked_at":"2026-04-08T14:31:17.202Z","response_time":54,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["error-handling","golang","slog","stacktrace"],"created_at":"2026-04-08T17:03:07.250Z","updated_at":"2026-04-08T17:03:28.293Z","avatar_url":"https://github.com/wood-jp.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# xerrors\n\n\u003c!-- badges --\u003e\n[![Go Version](https://img.shields.io/github/go-mod/go-version/wood-jp/xerrors)](https://pkg.go.dev/github.com/wood-jp/xerrors)\n[![CI](https://github.com/wood-jp/xerrors/actions/workflows/ci.yml/badge.svg)](https://github.com/wood-jp/xerrors/actions/workflows/ci.yml)\n[![Coverage Status](https://coveralls.io/repos/github/wood-jp/xerrors/badge.svg?branch=main)](https://coveralls.io/github/wood-jp/xerrors?branch=main)\n[![Release](https://img.shields.io/github/v/release/wood-jp/xerrors)](https://github.com/wood-jp/xerrors/releases)\n[![Go Report Card](https://goreportcard.com/badge/github.com/wood-jp/xerrors)](https://goreportcard.com/report/github.com/wood-jp/xerrors)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n[![Go Reference](https://pkg.go.dev/badge/github.com/wood-jp/xerrors.svg)](https://pkg.go.dev/github.com/wood-jp/xerrors)\n\u003c!-- /badges --\u003e\n\nWrap any error with any data stucture using generics; automatically log that data, or extract directly it later. Loggable stacktraces out of the box.\n\n- [Stability](#stability)\n- [Installation](#installation)\n- [Core package](#core-package)\n- [Subpackages](#subpackages)\n  - [errclass](#errclass)\n  - [errcontext](#errcontext)\n  - [stacktrace](#stacktrace)\n  - [calm](#calm)\n  - [errgroup](#errgroup)\n- [Performance](#performance)\n- [Contributing](#contributing)\n- [Security](#security)\n- [Attribution](#attribution)\n\n## Stability\n\nv1.x releases make no breaking changes to exported APIs. New functionality may be added in minor releases; patches are bug fixes, or administrative work only.\n\n## Installation\n\nGo 1.26.1 or later.\n\n```bash\ngo get github.com/wood-jp/xerrors\n```\n\n## Core package\n\n### Attaching data to an error\n\n`Extend` wraps an error with a value of any type. Passing `nil` returns `nil`.\n\n```go\ntype RequestContext struct {\n    UserID    string\n    RequestID string\n}\n\nerr := xerrors.Extend(RequestContext{UserID: \"123\", RequestID: \"abc\"}, originalErr)\n```\n\n### Getting it back out\n\n`Extract` walks the error chain and returns the first value of the requested type. If the same type has been extended more than once, you get the outermost one.\n\n```go\nif rctx, ok := xerrors.Extract[RequestContext](err); ok {\n    fmt.Println(rctx.UserID)\n}\n```\n\nThis works through multiple layers of wrapping:\n\n```go\nerr := xerrors.Extend(myData, originalErr)\nwrapped := fmt.Errorf(\"operation failed: %w\", err)\ndata, ok := xerrors.Extract[MyData](wrapped) // still works\n```\n\n### Structured logging\n\n`ExtendedError` implements `slog.LogValuer`, so logging a wrapped error works out of the box by walking the full chain and collecting everything into one flat structure:\n\n```json\n{\n  \"error\": \"something went wrong\",\n  \"error_detail\": {\n    \"class\": \"transient\",\n    \"stacktrace\": [...],\n    \"context\": { \"user_id\": \"123\" }\n  }\n}\n```\n\n`xerrors.Log(err)` returns that as a ready-to-use `slog.Attr` with the key `\"error\"`. Just drop it into any slog call:\n\n```go\nlogger.Error(\"request failed\", xerrors.Log(err))\n```\n\nData types contribute to `error_detail` by implementing `slog.LogValuer` and returning a group value. The attrs in that group are merged directly into `error_detail`. Types that don't implement `slog.LogValuer`, or whose `LogValue` doesn't resolve to a group, fall back to a single `\"data\"` key. See sub-packages for examples.\n\n### Edge cases\n\n- `Extend(nil)` returns nil\n- If you extend the same type more than once, `Extract` returns the outermost one\n- Type aliases are distinct: `type A int` and `type B int` don't match each other\n\n\u003e **WARNING:** This should not be used in conjuction with `errors.Join` as the resulting joined error may have unexpected behavior.\n\n## Subpackages\n\n### errclass\n\n```text\ngithub.com/wood-jp/xerrors/errclass\n```\n\nAttaches a severity class to an error so callers can decide whether to retry.\n\nClasses are ordered by severity:\n\n| Class | Description |\n| --- | --- |\n| `Nil` | No error (nil) |\n| `Unknown` | Unclassified (zero value) |\n| `Transient` | May succeed on retry |\n| `Persistent` | Will not resolve on retry |\n| `Panic` | Came from a recovered panic |\n\nBy default, `WrapAs` always applies the class unconditionally:\n\n```go\n// Wraps regardless of whether err already has a class\nerr := errclass.WrapAs(err, errclass.Transient)\n\nclass := errclass.GetClass(err)\nif class == errclass.Transient {\n    // retry\n}\n```\n\nTwo options let you restrict when wrapping happens:\n\n```go\n// Only classify if the error has no class yet — leaves already-classified errors alone\nerr = errclass.WrapAs(err, errclass.Persistent, errclass.WithOnlyUnknown())\n\n// Only classify if the new class is more severe than the current one — useful for escalation\nerr = errclass.WrapAs(err, errclass.Panic, errclass.WithOnlyMoreSevere())\n```\n\n`Class` implements `slog.LogValuer`. It shows up as `\"class\": \"transient\"` in flat log output.\n\n`errors.Join` is not supported. Class information on individual errors may be lost when combining into a joined error.\n\n---\n\n### errcontext\n\n```text\ngithub.com/wood-jp/xerrors/errcontext\n```\n\nAttaches `slog.Attr` key-value pairs to an error. Useful for carrying request-scoped fields through a call stack without threading them through every function signature.\n\n```go\n// Attach context\nerr := errcontext.Add(err, slog.String(\"user_id\", \"123\"), slog.Int(\"attempt\", 3))\n\n// Add more later — the existing map is updated in place, no extra wrapper\nerr = errcontext.Add(err, slog.String(\"request_id\", \"abc\"))\n\n// Pull it out\nctx := errcontext.Get(err)\nif ctx != nil {\n    attrs := ctx.Flatten() // sorted by key for deterministic output\n    slog.Info(\"request failed\", attrs...)\n}\n```\n\n`Context` implements `slog.LogValuer`. Attached keys appear under `\"context\"` in flat log output.\n\n`Add` with nil returns nil. `Add` with no attrs is a no-op. Duplicate keys use last-write-wins. `errors.Join` is not supported.\n\n---\n\n### stacktrace\n\n```text\ngithub.com/wood-jp/xerrors/stacktrace\n```\n\nCaptures a stack trace where `Wrap` is called and attaches it to the error. If the error already has a trace, `Wrap` is a no-op.\n\n`StackTrace` implements `slog.LogValuer`, and appears as a `\"stacktrace\"` array in flat log output. For example:\n\n```go\nvar errTest = errors.New(\"something went wrong\")\n\nfunc c() error {\n    return stacktrace.Wrap(errclass.WrapAs(errTest, errclass.Transient))\n}\n\nfunc b() error { return c() }\nfunc a() error { return b() }\n\nerr := a()\nlogger.Error(\"request failed\", xerrors.Log(err))\n```\n\nOutputs a log similar to:\n\n```json\n{\n  \"level\": \"ERROR\",\n  \"msg\": \"request failed\",\n  \"error\": {\n    \"error\": \"something went wrong\",\n    \"error_detail\": {\n      \"class\": \"transient\",\n      \"stacktrace\": [\n        {\"func\": \"main.c\", \"line\": 16, \"source\": \"main.go\"},\n        {\"func\": \"main.b\", \"line\": 20, \"source\": \"main.go\"},\n        {\"func\": \"main.a\", \"line\": 24, \"source\": \"main.go\"},\n        {\"func\": \"main.main\", \"line\": 31, \"source\": \"main.go\"}\n      ]\n    }\n  }\n}\n```\n\nHowever, if you wish to directly get at the stack trace data, you can pull the trace back out with `Extract`:\n\n```go\nif st := stacktrace.Extract(err); st != nil {\n    // st is a []Frame with File, LineNumber, Function\n}\n```\n\nAlternatively, if you don't want to capture any stack traces but want to keep the code around, just disable them globally:\n\n```go\nstacktrace.Disabled.Store(true)\n```\n\nThis results in all `Wrap` calls becoming no-ops.\n\n### calm\n\n```text\ngithub.com/wood-jp/xerrors/calm\n```\n\nWraps a function call so that any panic is recovered and returned as an error with a\nstack trace and an [`errclass.Panic`](#errclass) classification.\n\n```go\nerr := calm.Unpanic(func() error {\n    // code that might panic\n    return doSomething()\n})\nif errclass.GetClass(err) == errclass.Panic {\n    // handle recovered panic\n}\n```\n\n`Unpanic` returns nil if f returns nil. If f panics, the panic value is wrapped with\n`fmt.Errorf(\"panic: %v\", r)`, given a stack trace starting at the panic site, and classified as `errclass.Panic`.\n\nIf the `panic` was called with an error argument, the panic value is wrapped with `fmt.Errorf(\"panic: %v\", r)`, preserving the original error.\n\n\u003e **WARNING:** It is not possible to recover from a panic in a goroutine spawned by\n\u003e `f()`. Goroutines created inside `f` must guard themselves against panics.\n\n### errgroup\n\n```text\ngithub.com/wood-jp/xerrors/errgroup\n```\n\nWraps [`golang.org/x/sync/errgroup`](https://pkg.go.dev/golang.org/x/sync/errgroup) so\nthat panics inside goroutines are recovered and returned as errors rather than crashing the\nprogram. Uses [`calm.Unpanic`](#calm) internally, so recovered panics carry a stack trace\nand an [`errclass.Panic`](#errclass) classification.\n\n```go\ng := errgroup.New()\ng.Go(func() error {\n    return doSomething() // panics are caught and returned as errors\n})\nif err := g.Wait(); err != nil {\n    if errclass.GetClass(err) == errclass.Panic {\n        // handle recovered panic\n    }\n}\n```\n\n`WithContext` works the same as upstream: the derived context is cancelled the first time a\ngoroutine returns a non-nil error (including a recovered panic), or when `Wait` returns.\n\n`SetLimit` and `TryGo` are also available and behave identically to the upstream package,\nwith the same panic-recovery guarantee.\n\n\u003e **WARNING:** Panics in goroutines spawned _inside_ `f()` are not recovered. Goroutines\n\u003e created within `f` must guard themselves — use [`calm.Unpanic`](#calm) or call\n\u003e `g.Go` again from within `f`.\n\n## Performance\n\nBenchmarks cover the three operations users care about: stack capture, generic wrapping/extraction, and context attachment. Run them yourself with:\n\n```bash\njust bench\n```\n\nResults on an Intel Core Ultra 7 155H (Go 1.26.1, linux/amd64, `-count=3`):\n\n```text\ngoos: linux\ngoarch: amd64\ncpu: Intel(R) Core(TM) Ultra 7 155H\n\npkg: github.com/wood-jp/xerrors/stacktrace\nBenchmarkWrap_New-22                \t  804999\t      1314 ns/op\t     880 B/op\t   5 allocs/op\nBenchmarkWrap_Existing-22           \t64795417\t        19 ns/op\t       0 B/op\t   0 allocs/op\nBenchmarkWrap_New_Deep-22           \t  497211\t      2772 ns/op\t    1104 B/op\t   5 allocs/op\nBenchmarkWrap_Existing_Deep-22      \t33198662\t        31 ns/op\t       0 B/op\t   0 allocs/op\n\npkg: github.com/wood-jp/xerrors\nBenchmarkExtend-22                  \t34270780\t        32 ns/op\t      48 B/op\t   1 allocs/op\nBenchmarkExtract_Shallow-22         \t89609863\t        11 ns/op\t       0 B/op\t   0 allocs/op\nBenchmarkExtract_Deep-22            \t28955666\t        42 ns/op\t       0 B/op\t   0 allocs/op\nBenchmarkLog-22                     \t 1398642\t       857 ns/op\t     960 B/op\t  20 allocs/op\n\npkg: github.com/wood-jp/xerrors/errcontext\nBenchmarkAdd_New-22                 \t 5845732\t       207 ns/op\t     440 B/op\t   4 allocs/op\nBenchmarkAdd_Existing-22            \t48651021\t        22 ns/op\t       0 B/op\t   0 allocs/op\nBenchmarkAdd_Existing_Deep-22       \t35166369\t        36 ns/op\t       0 B/op\t   0 allocs/op\nBenchmarkFlatten-22                 \t 2332621\t       511 ns/op\t     512 B/op\t   8 allocs/op\n```\n\nAs one might expect, call-depth (for stacktraces) and error-chain depth impact the actual costs. The \"deep\" benchmarks here only have depth/length of 5 for illustrative purposes.\n\nActually obtaining a stack trace is expensive, but only happens once in the call-chain. Re-wrapping an already-traced error is a no-op (aside walking the error chain).\n\nAdding error context is also very cheap after the first. It also has an error-chain depth traversal cost if adding context at different call sites.\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md).\n\n## Security\n\nSee [SECURITY.md](SECURITY.md).\n\n## Attribution\n\n*This library is a simplified fork of one written by [wood-jp](https://github.com/wood-jp) at [Zircuit](https://www.zircuit.com/). The original code is available here: [zkr-go-common-public/xerrors](https://github.com/zircuit-labs/zkr-go-common-public/tree/main/xerrors)*\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwood-jp%2Fxerrors","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwood-jp%2Fxerrors","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwood-jp%2Fxerrors/lists"}