{"id":22650015,"url":"https://github.com/pauliorandall/go-trackerr","last_synced_at":"2026-05-28T06:05:45.951Z","repository":{"id":64301070,"uuid":"572010081","full_name":"PaulioRandall/go-trackerr","owner":"PaulioRandall","description":"Go package for crafting archetypal errors and explorable stack traces","archived":false,"fork":false,"pushed_at":"2023-06-03T15:02:18.000Z","size":171,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"trunk","last_synced_at":"2025-02-13T09:39:01.856Z","etag":null,"topics":["debugging","error-handling","go","golang","testing"],"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/PaulioRandall.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-11-29T11:15:16.000Z","updated_at":"2023-03-09T17:08:27.000Z","dependencies_parsed_at":"2024-06-20T05:15:56.003Z","dependency_job_id":"f9544f7f-1680-4ecf-b937-ce9428fcfd32","html_url":"https://github.com/PaulioRandall/go-trackerr","commit_stats":null,"previous_names":["pauliorandall/trackable","pauliorandall/go-trackable"],"tags_count":20,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PaulioRandall%2Fgo-trackerr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PaulioRandall%2Fgo-trackerr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PaulioRandall%2Fgo-trackerr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PaulioRandall%2Fgo-trackerr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/PaulioRandall","download_url":"https://codeload.github.com/PaulioRandall/go-trackerr/tar.gz/refs/heads/trunk","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246150459,"owners_count":20731419,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["debugging","error-handling","go","golang","testing"],"created_at":"2024-12-09T08:30:02.336Z","updated_at":"2025-12-15T15:59:24.393Z","avatar_url":"https://github.com/PaulioRandall.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Trackerr\n\nPackage trackerr aims to facilitate creation of referenceable errors and elegant stack traces.\n\nIt was crafted in frustration trying to navigate Go's printed error stacks and the challenge of reliably asserting specific error types while testing.\n\nI hope the code speaks mostly for itself so you don't have to trawl through my ramblings.\n\n## API\n\n```go\nimport (\n\t// Package imported is just called 'trackerr' \n\t\"github.com/PaulioRandall/go-trackerr\"\n)\n```\n\n**Please note:** `TrackedError` and `UntrackedError` are structs but I've specified them here as interfaces for documentation purposes.\n\n```go\nvar (\n    // ErrTodo for specifying a TODO.\n    ErrTodo = New(\"TODO: Implementation needed\")\n\n    // ErrBug for the site of known bugs\n    ErrBug = New(\"BUG: Fix needed\")\n\n    // ErrInsane for sanity checking.\n    ErrInsane = New(\"Sanity check failed!!\")\n)\n\nfunc New(msg string, args ...any) TrackedError {}\nfunc Track(msg string, args ...any) TrackedError {}\nfunc Untracked(msg string, args ...any) UntrackedError {}\n\nfunc All(e error, targets ...error) bool\nfunc AllOrdered(e error, targets ...error) bool\nfunc Any(e error, targets ...error) bool\nfunc HasTracked(e error) bool\nfunc Is(e, target error) bool\nfunc IsTracked(e error) bool\nfunc IsTrackerr(e error) bool\nfunc Unwrap(e error) error\n\nfunc Stack(rootCause error, errs ...ErrorThatWraps) error\nfunc SliceStack(e error) []error\nfunc Squash(e error) error\nfunc Squashf(e error, f ErrorFormatter) error\nfunc ErrorStack(e error) string\nfunc ErrorStackf(e error, f ErrorFormatter) string\nfunc ErrorWithoutCause(e error) string\n\nfunc Debug(e error) (int, error)\nfunc DebugPanic(catch *error)\n\nfunc Initialised()\n\ntype ErrorFormatter func(errMsg string, e error, isFirst bool) string\n\ntype ErrorThatWraps interface {\n\terror\n\tCausedBy(rootCause error, causes ...ErrorThatWraps) error\n}\n\ntype TrackedError interface { // Actually a struct in code\n\tErrorThatWraps\n\n\tError() string\n\n\tBecause(msg string, args ...any) error\n\tBecauseOf(rootCause error, msg string, args ...any) error\n\tCausedBy(rootCause error, causes ...ErrorThatWraps) error\n\n\tIs(error) bool\n\tUnwrap() error\n}\n\ntype UntrackedError interface { // Actually a struct in code\n\tErrorThatWraps\n\n\tError() string\n\n\tBecause(msg string, args ...any) error\n\tBecauseOf(rootCause error, msg string, args ...any) error\n\tCausedBy(rootCause error, causes ...ErrorThatWraps) error\n\n\tUnwrap() error\n}\n\ntype Realm interface {\n\tNew(msg string, args ...any) *TrackedError\n\tTrack(msg string, args ...any) *TrackedError\n}\n\ntype IntRealm struct {}\n```\n\n**Tracked errors should be package variables**\n\nIt's important to define errors created via `New` and `Track` as package scooped (global) or you won't be able to reference them. It is not recommended to create trackable errors after initialisation but Realms exist for such cases.\n\n**Wrapping errors**\n\nYou can return a tracked or untracked error directly but it's recommended to call one of the receiving functions `CausedBy`, `Because`, `BecauseOf`, or `ContextFor` with additional information.\n\n```go\nvar (\n\tErrLoadingData = trackerr.New(\"Failed to load data\")\n\tErrOpeningDatabase = trackerr.New(\"Could not open database\")\n\n\tdbFile = \"./data/db.sqlite\"\n)\n\nfunc Err() error {\n\treturn ErrLoadingData\n}\n\nfunc CausedBy() error {\n\treturn ErrLoadingData.CausedBy(ErrOpeningDatabase)\n}\n\nfunc Because() error {\n\treturn ErrLoadingData.Because(\"Database file '%s' not found\", dbFile)\n}\n\nfunc BecauseOf() error {\n\te := trackerr.Untracked(\"Database file '%s' not found\", dbFile)\n\treturn ErrLoadingData.BecauseOf(e, \"Could not open database\")\n}\n\nfunc ContextFor() error {\n\te := trackerr.Untracked(\"Database file '%s' not found\", dbFile)\n\treturn ErrLoadingData.ContextFor(ErrOpeningDatabase, e)\n}\n```\n\n**Prevent creating tracked errors after program initialisation**\n\nIt's also recommended to call `Initialised` from an init function in package main to prevent the creation of trackable errors after program initialisation.\n\n```go\npackage main\n\nimport (\n\t\"github.com/PaulioRandall/go-trackerr\"\n)\n\nvar ErrForNoReason = trackerr.New(\"Failed for no reason\")\n\nfunc init() {\n\ttrackerr.Initialised()\n}\n\nfunc main() {\n\t// Bad, will panic\n\te = trackerr.New(\"I felt like it\")\n\n\t_ = e\n}\n```\n\n**Debugging**\n\nFor manual debugging there's `trackerr.Debug` which will print a readable stack trace.\n\n```go\nfunc Debug() {\n\ta := trackerr.UntrackedError(\"Failed to load data\")\n\tb := trackerr.UntrackedError(\"Could not open database\")\n\tc := trackerr.UntrackedError(\"Database file not found\")\n\n\te := Stack(a, b, c)\n\n\ttrackerr.Debug(e)\n\n\t// [DEBUG ERROR]\n\t// Failed to load data\n\t// ⤷ Could not open database\n\t// ⤷ Database file not found\n}\n```\n\nAlternatively the deferable `trackerr.DebugPanic(nil)` will recover from a panic, print the error (if it is one), then resume the panic.\n\n```go\nfunc DebugPanic() {\n\tdefer trackerr.DebugPanic(nil)\n\n\ta := trackerr.UntrackedError(\"Failed to load data\")\n\tb := trackerr.UntrackedError(\"Could not open database\")\n\tc := trackerr.UntrackedError(\"Database file not found\")\n\n\te := Stack(a, b, c)\n\tpanic(e)\n\n\t// [DEBUG ERROR]\n\t// Failed to load data\n\t// ⤷ Could not open database\n\t// ⤷ Database file not found\n}\n```\n\nPassing a pointer to an error `trackerr.DebugPanic(\u0026e)` will prevent the panic resuming and instead set it as the value pointed to by the pointer. \n\n```go\nfunc DebugPanic() (e error) {\n\tdefer trackerr.DebugPanic(\u0026e)\n\n\t...\n}\n```\n\n**Custom errors**\n\nYou may also craft your own error types and wrap or be wrapped by trackerr errors.\n\n```go\ntype myError struct {\n\tmsg string\n\tcause error\n}\n\nfunc (e myError) CausedBy(other error) error {\n\te.cause = other\n\treturn e\n}\n\nfunc (e myError) Unwrap() error {\n\treturn e.cause\n}\n\nvar (\n\tErrLoadingData = trackerr.New(\"Failed to load data\")\n\tErrFileNotFound = trackerr.New(\"Database file not found\")\n)\n\nfunc main() {\n\te := myError{ msg: \"Could not open database\" }\n\te = ErrLoadingData.ContextFor(ErrFileNotFound, e)\n\t_ = e\n}\n```\n\n### Testing\n\nOne place trackerr becomes useful is when asserting errors in tests.\n\nTrackerr assigns errors there own private unique identifiers which are used for comparison by `errors.Is` and trackerr's utility functions. This separates the concerns of communicating with humans from asserting that specific errors occur when they should.\n\n```go\n// csvreader.go\n\nimport (\n\t\"errors\"\n)\n\nvar ErrParsingCSV = trackerr.New(\"Could not parse CSV\")\n\nfunc ReadCSV(file string) error {\n\t...\n\n\treturn ErrParsingCSV\n}\n```\n\n```go\n// csvreader_test.go\n\nimport (\n\t\"errors\"\n\t\"testing\"\n)\n\nfunc TestReadCSV_InvalidFormat(t *testing.T) {\n\te := ReadCSV(\"/path/to/csv/file\")\n\t\n\tif !errors.Is(e, ErrParsingCSV) {\n\t\tt.Log(\"Expected ErrParsingCSV error\")\n\t\tt.Fail()\n\t}\n}\n```\n\n## Design decisions\n\nThe design is largely usage lead and thus somewhat emergent. That is, I had projects requiring trackable errors to which I crafted structures and functions based on need.\n\n### Composition \u003e Framing\n\nThe package is designed to work in a compositional manner such that `trackerr.New`, `trackerr.Track`, and `errors.new` can be exchanged incrementally. Engineers may compose all their errors using trackerr or just the few that require tracking. Most of trackerr's utility functions work on the `error` interface so the underlying error types matter little.\n\nComposition is favoured over framing, when feasible, so the power to change and adapt, with needs and the times, remains in the hands of the consuming engineers. In so much as possible, minimising the _my way or the highway_ mentality which is core to commercial software but also rampant in open source tooling.\n\nIf my package no longer provides value for cost or if something better appears then it should be **incrementally** removable or replacable. I find that a good design is one that can change easily. My preference for changability, Continuous Integration (CI), and Continuous Delivery (CD) certainly influenced these decisions.\n\n### Why not string equality?\n\nMany programmers test assert using error messages (strings) but I've found this to be unreliable, reduces changability, and leaves me feeling less than confident in my code; and testing is all about gaining confidence.\n\nCommunicating aaccurate and relevant information to humans can be quite a fraught affair so I'd like to maximise the ease of improving and rewriting error messages without having to worry about breaking tests.\n\n### Why not pointer equality?\n\nComparing pointers is better than comparing text but this means package scooped errors must be immutable, thus cannot have a cause attached to them or be wrapped. The receiving functions of `TrackedError` and `UntrackedError` produce copies of themselves (including their IDs) that allows the attachment of causes while keeping the equality checking. `errors.Is(copy, original)` still returns true as private unique identifiers are compared, not string messages or pointers.\n\nUnfortunately, this means `copy == original` will always return false. This is not much of a sacrifice as error pointer comparisons lost favour with the introduction of error wrapping ([Go 1.13](https://tip.golang.org/doc/go1.13#error_wrapping)). Use `errors.Is`, `trackerr.Is`, or one of trackerr's other utility functions instead.\n\n## Checking out (in both senses)\n\n```bash\ngit clone https://github.com/PaulioRandall/go-trackerr.git\ncd go-trackerr\n```\n\nStandard Go commands can be used from here but my `./godo` script eases things:\n\n```bash\n./godo [help]   # Print usage\n./godo doc[s]   # Fire up documentation server\n./godo clean    # Clean Go caches\n./godo test     # fmt -\u003e test -\u003e vet\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpauliorandall%2Fgo-trackerr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpauliorandall%2Fgo-trackerr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpauliorandall%2Fgo-trackerr/lists"}