{"id":"a3b9a40f-3227-4607-80ab-67ecb77d1f6e","shortId":"cy3h9H","kind":"skill","title":"golang-samber-oops","tagline":"Structured error handling in Golang with samber/oops — error builders, stack traces, error codes, error context, error wrapping, error attributes, user-facing vs developer messages, panic recovery, and logger integration. Apply when using or adopting samber/oops, or when the code","description":"**Persona:** You are a Go engineer who treats errors as structured data. Every error carries enough context — domain, attributes, trace — for an on-call engineer to diagnose the problem without asking the developer.\n\n# samber/oops Structured Error Handling\n\n**samber/oops** is a drop-in replacement for Go's standard error handling that adds structured context, stack traces, error codes, public messages, and panic recovery. Variable data goes in `.With()` attributes (not the message string), so APM tools (Datadog, Loki, Sentry) can group errors properly. Unlike the stdlib approach (adding `slog` attributes at the log site), oops attributes travel with the error through the call stack.\n\n## Why use samber/oops\n\nStandard Go errors lack context — you see `connection failed` but not which user triggered it, what query was running, or the full call stack. `samber/oops` provides:\n\n- **Structured context** — key-value attributes on any error\n- **Stack traces** — automatic call stack capture\n- **Error codes** — machine-readable identifiers\n- **Public messages** — user-safe messages separate from technical details\n- **Low-cardinality messages** — variable data in `.With()` attributes, not the message string, so APM tools group errors properly\n\nThis skill is not exhaustive. Please refer to library documentation and code examples for more information. Context7 can help as a discoverability platform.\n\n## Core pattern: Error builder chain\n\nAll `oops` errors use a fluent builder pattern:\n\n```go\nerr := oops.\n    In(\"user-service\").           // domain/feature\n    Tags(\"database\", \"postgres\").  // categorization\n    Code(\"network_failure\").       // machine-readable identifier\n    User(\"user-123\", \"email\", \"foo@bar.com\").  // user context\n    With(\"query\", query).          // custom attributes\n    Errorf(\"failed to fetch user: %s\", \"timeout\")\n```\n\nTerminal methods:\n\n- `.Errorf(format, args...)` — create a new error\n- `.Wrap(err)` — wrap an existing error\n- `.Wrapf(err, format, args...)` — wrap with a message\n- `.Join(err1, err2, ...)` — combine multiple errors\n- `.Recover(fn)` / `.Recoverf(fn, format, args...)` — convert panic to error\n\n### Error builder methods\n\n| Methods | Use case |\n| --- | --- |\n| `.With(\"key\", value)` | Add custom key-value attribute (lazy `func() any` values supported) |\n| `.WithContext(ctx, \"key1\", \"key2\")` | Extract values from Go context into attributes (lazy values supported) |\n| `.In(\"domain\")` | Set the feature/service/domain |\n| `.Tags(\"auth\", \"sql\")` | Add categorization tags (query with `err.HasTag(\"tag\")`) |\n| `.Code(\"iam_authz_missing_permission\")` | Set machine-readable error identifier/slug |\n| `.Public(\"Could not fetch user.\")` | Set user-safe message (separate from technical details) |\n| `.Hint(\"Runbook: https://doc.acme.org/doc/abcd.md\")` | Add debugging hint for developers |\n| `.Owner(\"team/slack\")` | Identify responsible team/owner |\n| `.User(id, \"k\", \"v\")` | Add user identifier and attributes |\n| `.Tenant(id, \"k\", \"v\")` | Add tenant/organization context and attributes |\n| `.Trace(id)` | Add trace / correlation ID (default: ULID) |\n| `.Span(id)` | Add span ID representing a unit of work/operation (default: ULID) |\n| `.Time(t)` | Override error timestamp (default: `time.Now()`) |\n| `.Since(t)` | Set duration based on time since `t` (exposed via `err.Duration()`) |\n| `.Duration(d)` | Set explicit error duration |\n| `.Request(req, includeBody)` | Attach `*http.Request` (optionally including body) |\n| `.Response(res, includeBody)` | Attach `*http.Response` (optionally including body) |\n| `oops.FromContext(ctx)` | Start from an `OopsErrorBuilder` stored in a Go context |\n\n## Common scenarios\n\n### Database/repository layer\n\n```go\nfunc (r *UserRepository) FetchUser(id string) (*User, error) {\n    query := \"SELECT * FROM users WHERE id = $1\"\n    row, err := r.db.Query(query, id)\n    if err != nil {\n        return nil, oops.\n            In(\"user-repository\").\n            Tags(\"database\", \"postgres\").\n            With(\"query\", query).\n            With(\"user_id\", id).\n            Wrapf(err, \"failed to fetch user from database\")\n    }\n    // ...\n}\n```\n\n### HTTP handler layer\n\n```go\nfunc (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) {\n    userID := getUserID(r)\n\n    err := h.service.CreateUser(r.Context(), userID)\n    if err != nil {\n        return oops.\n            In(\"http-handler\").\n            Tags(\"endpoint\", \"/users\").\n            Request(r, false).\n            User(userID).\n            Wrapf(err, \"create user failed\")\n    }\n\n    w.WriteHeader(http.StatusCreated)\n}\n```\n\n### Service layer with reusable builder\n\n```go\nfunc (s *UserService) CreateOrder(ctx context.Context, req CreateOrderRequest) error {\n    builder := oops.\n        In(\"order-service\").\n        Tags(\"orders\", \"checkout\").\n        Tenant(req.TenantID, \"plan\", req.Plan).\n        User(req.UserID, \"email\", req.UserEmail)\n\n    product, err := s.catalog.GetProduct(ctx, req.ProductID)\n    if err != nil {\n        return builder.\n            With(\"product_id\", req.ProductID).\n            Wrapf(err, \"product lookup failed\")\n    }\n\n    if product.Stock < req.Quantity {\n        return builder.\n            Code(\"insufficient_stock\").\n            Public(\"Not enough items in stock.\").\n            With(\"requested\", req.Quantity).\n            With(\"available\", product.Stock).\n            Errorf(\"insufficient stock for product %s\", req.ProductID)\n    }\n\n    return nil\n}\n```\n\n## Error wrapping best practices\n\n### DO: Wrap directly, no nil check needed\n\n```go\n// ✓ Good — Wrap returns nil if err is nil\nreturn oops.Wrapf(err, \"operation failed\")\n\n// ✗ Bad — unnecessary nil check\nif err != nil {\n    return oops.Wrapf(err, \"operation failed\")\n}\nreturn nil\n```\n\n### DO: Add context at each layer\n\nEach architectural layer SHOULD add context via Wrap/Wrapf — at least once per package boundary (not necessarily at every function call).\n\n```go\n// ✓ Good — each layer adds relevant context\nfunc Controller() error {\n    return oops.In(\"controller\").Trace(traceID).Wrapf(Service(), \"user request failed\")\n}\n\nfunc Service() error {\n    return oops.In(\"service\").With(\"op\", \"create_user\").Wrapf(Repository(), \"db operation failed\")\n}\n\nfunc Repository() error {\n    return oops.In(\"repository\").Tags(\"database\", \"postgres\").Errorf(\"connection timeout\")\n}\n```\n\n### DO: Keep error messages low-cardinality\n\nError messages MUST be low-cardinality for APM aggregation. Interpolating variable data into the message breaks grouping in Datadog, Loki, Sentry.\n\n```go\n// ✗ Bad — high-cardinality, breaks APM grouping\noops.Errorf(\"failed to process user %s in tenant %s\", userID, tenantID)\n\n// ✓ Good — static message + structured attributes\noops.With(\"user_id\", userID).With(\"tenant_id\", tenantID).Errorf(\"failed to process user\")\n```\n\n## Panic recovery\n\n`oops.Recover()` MUST be used in goroutine boundaries. Convert panics to structured errors:\n\n```go\nfunc ProcessData(data string) (err error) {\n    return oops.\n        In(\"data-processor\").\n        Code(\"panic_recovered\").\n        Hint(\"Check input data format and dependencies\").\n        With(\"panic_value\", r).\n        Recover(func() {\n            riskyOperation(data)\n        })\n}\n```\n\n## Accessing error information\n\n`samber/oops` errors implement the standard `error` interface. Access additional info:\n\n```go\nif oopsErr, ok := err.(oops.OopsError); ok {\n    fmt.Println(\"Code:\", oopsErr.Code())\n    fmt.Println(\"Domain:\", oopsErr.Domain())\n    fmt.Println(\"Tags:\", oopsErr.Tags())\n    fmt.Println(\"Context:\", oopsErr.Context())\n    fmt.Println(\"Stacktrace:\", oopsErr.Stacktrace())\n}\n\n// Get public-facing message with fallback\npublicMsg := oops.GetPublic(err, \"Something went wrong\")\n```\n\n### Output formats\n\n```go\nfmt.Printf(\"%+v\\n\", err)       // verbose with stack trace\nbytes, _ := json.Marshal(err)  // JSON for logging\nslog.Error(err.Error(), slog.Any(\"error\", err))  // slog integration\n```\n\n## Context propagation\n\nCarry error context through Go contexts:\n\n```go\nfunc middleware(next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        builder := oops.\n            In(\"http\").\n            Request(r, false).\n            Trace(r.Header.Get(\"X-Trace-ID\"))\n\n        ctx := oops.WithBuilder(r.Context(), builder)\n        next.ServeHTTP(w, r.WithContext(ctx))\n    })\n}\n\nfunc handler(ctx context.Context) error {\n    return oops.FromContext(ctx).Tags(\"handler\", \"users\").Errorf(\"something failed\")\n}\n```\n\nFor assertions, configuration, and additional logger examples, see [Advanced patterns](./references/advanced.md).\n\n## References\n\n- [github.com/samber/oops](https://github.com/samber/oops)\n- [pkg.go.dev/github.com/samber/oops](https://pkg.go.dev/github.com/samber/oops)\n\n## Cross-References\n\n- → See `samber/cc-skills-golang@golang-error-handling` skill for general error handling patterns\n- → See `samber/cc-skills-golang@golang-observability` skill for logger integration and structured logging","tags":["golang","samber","oops","skills","agent","agent-skills","antigravity","claude","claude-code","code","codex","coding"],"capabilities":["skill","source-samber","skill-golang-samber-oops","topic-agent","topic-agent-skills","topic-antigravity","topic-claude","topic-claude-code","topic-code","topic-codex","topic-coding","topic-copilot","topic-cursor","topic-gemini","topic-gemini-cli-extension"],"categories":["cc-skills-golang"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/samber/cc-skills-golang/golang-samber-oops","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add samber/cc-skills-golang","source_repo":"https://github.com/samber/cc-skills-golang","install_from":"skills.sh"}},"qualityScore":"0.700","qualityRationale":"deterministic score 0.70 from registry signals: · indexed on github topic:agent-skills · 1725 github stars · SKILL.md body (8,987 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-05-18T18:53:02.477Z","embedding":null,"createdAt":"2026-04-18T20:32:46.199Z","updatedAt":"2026-05-18T18:53:02.477Z","lastSeenAt":"2026-05-18T18:53:02.477Z","tsv":"'-123':286 '/doc/abcd.md':420 '/github.com/samber/oops](https://pkg.go.dev/github.com/samber/oops)':1065 '/references/advanced.md':1058 '/samber/oops](https://github.com/samber/oops)':1062 '/users':604 '1':540 'access':920,930 'ad':133 'add':97,351,384,421,435,444,451,459,737,746,766 'addit':931,1052 'adopt':39 'advanc':1056 'aggreg':825 'apm':120,224,824,844 'appli':35 'approach':132 'architectur':743 'arg':307,321,337 'ask':76 'assert':1049 'attach':497,505 'attribut':23,63,114,135,141,184,218,295,356,372,439,448,861 'auth':382 'authz':393 'automat':190 'avail':686 'bad':722,839 'base':480 'best':699 'bodi':501,509 'boundari':755,883 'break':832,843 'builder':13,255,263,343,621,632,658,672,1013,1029 'byte':979 'call':69,148,175,191,761 'captur':193 'cardin':212,815,822,842 'carri':59,994 'case':347 'categor':276,385 'chain':256 'check':706,725,906 'checkout':640 'code':17,44,103,195,240,277,391,673,902,941 'combin':329 'common':521 'configur':1050 'connect':160,807 'context':19,61,99,157,180,290,370,446,520,738,747,768,950,992,996,999 'context.context':628,1037 'context7':245 'control':770,774 'convert':338,884 'core':252 'correl':453 'could':403 'creat':308,612,790 'createord':626 'createorderrequest':630 'createus':581 'cross':1067 'cross-refer':1066 'ctx':363,511,627,652,1026,1033,1036,1041 'custom':294,352 'd':489 'data':56,110,215,828,892,900,908,919 'data-processor':899 'databas':274,557,573,804 'database/repository':523 'datadog':122,835 'db':794 'debug':422 'default':455,467,474 'depend':911 'detail':209,415 'develop':28,78,425 'diagnos':72 'direct':703 'discover':250 'doc.acme.org':419 'doc.acme.org/doc/abcd.md':418 'document':238 'domain':62,377,944 'domain/feature':272 'drop':87 'drop-in':86 'durat':479,488,493 'email':287,647 'endpoint':603 'engin':50,70 'enough':60,678 'err':266,313,319,542,547,567,589,594,611,650,655,664,714,719,727,731,894,937,964,974,981,989 'err.duration':487 'err.error':986 'err.hastag':389 'err1':327 'err2':328 'error':6,12,16,18,20,22,53,58,81,94,102,127,145,155,187,194,227,254,259,311,317,331,341,342,400,472,492,533,631,697,771,784,799,811,816,888,895,921,924,928,988,995,1038,1073,1078 'errorf':296,305,688,806,870,1045 'everi':57,759 'exampl':241,1054 'exhaust':233 'exist':316 'explicit':491 'expos':485 'extract':366 'face':26,958 'fail':161,297,568,614,667,721,733,781,796,847,871,1047 'failur':279 'fallback':961 'fals':607,1019 'feature/service/domain':380 'fetch':299,405,570 'fetchus':529 'fluent':262 'fmt.printf':971 'fmt.println':940,943,946,949,952 'fn':333,335 'foo@bar.com':288 'format':306,320,336,909,969 'full':174 'func':358,526,578,623,769,782,797,890,917,1001,1008,1034 'function':760 'general':1077 'get':955 'getuserid':587 'github.com':1061 'github.com/samber/oops](https://github.com/samber/oops)':1060 'go':49,91,154,265,369,519,525,577,622,708,762,838,889,933,970,998,1000 'goe':111 'golang':2,9,1072,1084 'golang-error-handl':1071 'golang-observ':1083 'golang-samber-oop':1 'good':709,763,857 'goroutin':882 'group':126,226,833,845 'h':579 'h.service.createuser':590 'handl':7,82,95,1074,1079 'handler':575,580,601,1035,1043 'help':247 'high':841 'high-cardin':840 'hint':416,423,905 'http':574,600,1016 'http-handler':599 'http.handler':1004,1005 'http.handlerfunc':1007 'http.request':498,585,1012 'http.response':506 'http.responsewriter':583,1010 'http.statuscreated':616 'iam':392 'id':432,441,450,454,458,461,530,539,545,564,565,661,864,868,1025 'identifi':199,283,428,437 'identifier/slug':401 'implement':925 'includ':500,508 'includebodi':496,504 'info':932 'inform':244,922 'input':907 'insuffici':674,689 'integr':34,991,1089 'interfac':929 'interpol':826 'item':679 'join':326 'json':982 'json.marshal':980 'k':433,442 'keep':810 'key':182,349,354 'key-valu':181,353 'key1':364 'key2':365 'lack':156 'layer':524,576,618,741,744,765 'lazi':357,373 'least':751 'librari':237 'log':138,984,1092 'logger':33,1053,1088 'loki':123,836 'lookup':666 'low':211,814,821 'low-cardin':210,813,820 'machin':197,281,398 'machine-read':196,280,397 'messag':29,105,117,201,205,213,221,325,411,812,817,831,859,959 'method':304,344,345 'middlewar':1002 'miss':394 'multipl':330 'must':818,878 'n':973 'necessarili':757 'need':707 'network':278 'new':310 'next':1003 'next.servehttp':1030 'nil':548,550,595,656,696,705,712,716,724,728,735 'observ':1085 'ok':936,939 'on-cal':67 'oop':4,140,258,267,551,597,633,897,1014 'oops.errorf':846 'oops.fromcontext':510,1040 'oops.getpublic':963 'oops.in':773,786,801 'oops.oopserror':938 'oops.recover':877 'oops.with':862 'oops.withbuilder':1027 'oops.wrapf':718,730 'oopserr':935 'oopserr.code':942 'oopserr.context':951 'oopserr.domain':945 'oopserr.stacktrace':954 'oopserr.tags':948 'oopserrorbuild':515 'op':789 'oper':720,732,795 'option':499,507 'order':636,639 'order-servic':635 'output':968 'overrid':471 'owner':426 'packag':754 'panic':30,107,339,875,885,903,913 'pattern':253,264,1057,1080 'per':753 'permiss':395 'persona':45 'pkg.go.dev':1064 'pkg.go.dev/github.com/samber/oops](https://pkg.go.dev/github.com/samber/oops)':1063 'plan':643 'platform':251 'pleas':234 'postgr':275,558,805 'practic':700 'problem':74 'process':849,873 'processdata':891 'processor':901 'product':649,660,665,692 'product.stock':669,687 'propag':993 'proper':128,228 'provid':178 'public':104,200,402,676,957 'public-fac':956 'publicmsg':962 'queri':169,292,293,387,534,544,560,561 'r':527,584,588,606,915,1011,1018 'r.context':591,1028 'r.db.query':543 'r.header.get':1021 'r.withcontext':1032 'readabl':198,282,399 'recov':332,904,916 'recoverf':334 'recoveri':31,108,876 'refer':235,1059,1068 'relev':767 'replac':89 'repositori':555,793,798,802 'repres':462 'req':495,629 'req.plan':644 'req.productid':653,662,694 'req.quantity':670,684 'req.tenantid':642 'req.useremail':648 'req.userid':646 'request':494,605,683,780,1017 'res':503 'respons':429,502 'return':549,596,657,671,695,711,717,729,734,772,785,800,896,1006,1039 'reusabl':620 'riskyoper':918 'row':541 'run':171 'runbook':417 's.catalog.getproduct':651 'safe':204,410 'samber':3 'samber/cc-skills-golang':1070,1082 'samber/oops':11,40,79,83,152,177,923 'scenario':522 'see':159,1055,1069,1081 'select':535 'sentri':124,837 'separ':206,412 'servic':271,617,637,778,783,787 'set':378,396,407,478,490 'sinc':476,483 'site':139 'skill':230,1075,1086 'skill-golang-samber-oops' 'slog':134,990 'slog.any':987 'slog.error':985 'someth':965,1046 'source-samber' 'span':457,460 'sql':383 'stack':14,100,149,176,188,192,977 'stacktrac':953 'standard':93,153,927 'start':512 'static':858 'stdlib':131 'stock':675,681,690 'store':516 'string':118,222,531,893 'structur':5,55,80,98,179,860,887,1091 'support':361,375 'tag':273,381,386,390,556,602,638,803,947,1042 'team/owner':430 'team/slack':427 'technic':208,414 'tenant':440,641,853,867 'tenant/organization':445 'tenantid':856,869 'termin':303 'time':469,482 'time.now':475 'timeout':302,808 'timestamp':473 'tool':121,225 'topic-agent' 'topic-agent-skills' 'topic-antigravity' 'topic-claude' 'topic-claude-code' 'topic-code' 'topic-codex' 'topic-coding' 'topic-copilot' 'topic-cursor' 'topic-gemini' 'topic-gemini-cli-extension' 'trace':15,64,101,189,449,452,775,978,1020,1024 'traceid':776 'travel':142 'treat':52 'trigger':166 'ulid':456,468 'unit':464 'unlik':129 'unnecessari':723 'use':37,151,260,346,880 'user':25,165,203,270,284,285,289,300,406,409,431,436,532,537,554,563,571,608,613,645,779,791,850,863,874,1044 'user-fac':24 'user-repositori':553 'user-saf':202,408 'user-servic':269 'userid':586,592,609,855,865 'userrepositori':528 'userservic':625 'v':434,443,972 'valu':183,350,355,360,367,374,914 'variabl':109,214,827 'verbos':975 'via':486,748 'vs':27 'w':582,1009,1031 'w.writeheader':615 'went':966 'withcontext':362 'without':75 'work/operation':466 'wrap':21,312,314,322,698,702,710 'wrap/wrapf':749 'wrapf':318,566,610,663,777,792 'wrong':967 'x':1023 'x-trace-id':1022","prices":[{"id":"3569f729-2db7-45de-9e94-4be03683c5a6","listingId":"a3b9a40f-3227-4607-80ab-67ecb77d1f6e","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"samber","category":"cc-skills-golang","install_from":"skills.sh"},"createdAt":"2026-04-18T20:32:46.199Z"}],"sources":[{"listingId":"a3b9a40f-3227-4607-80ab-67ecb77d1f6e","source":"github","sourceId":"samber/cc-skills-golang/golang-samber-oops","sourceUrl":"https://github.com/samber/cc-skills-golang/tree/main/skills/golang-samber-oops","isPrimary":false,"firstSeenAt":"2026-04-18T21:55:20.740Z","lastSeenAt":"2026-05-18T18:53:02.477Z"},{"listingId":"a3b9a40f-3227-4607-80ab-67ecb77d1f6e","source":"skills_sh","sourceId":"samber/cc-skills-golang/golang-samber-oops","sourceUrl":"https://skills.sh/samber/cc-skills-golang/golang-samber-oops","isPrimary":true,"firstSeenAt":"2026-04-18T20:32:46.199Z","lastSeenAt":"2026-05-07T22:40:28.341Z"}],"details":{"listingId":"a3b9a40f-3227-4607-80ab-67ecb77d1f6e","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"samber","slug":"golang-samber-oops","github":{"repo":"samber/cc-skills-golang","stars":1725,"topics":["agent","agent-skills","ai","antigravity","claude","claude-code","code","codex","coding","copilot","cursor","gemini","gemini-cli-extension","openclaw","opencode","plugin","skills","skillsmp","vibe-coding"],"license":"mit","html_url":"https://github.com/samber/cc-skills-golang","pushed_at":"2026-05-18T17:36:00Z","description":"🧑‍🎨 A collection of Golang agentic skills that works","skill_md_sha":"3fd312314726b77bdab9cba4cdf68f561c0bff29","skill_md_path":"skills/golang-samber-oops/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/samber/cc-skills-golang/tree/main/skills/golang-samber-oops"},"layout":"multi","source":"github","category":"cc-skills-golang","frontmatter":{"name":"golang-samber-oops","license":"MIT","description":"Structured error handling in Golang with samber/oops — error builders, stack traces, error codes, error context, error wrapping, error attributes, user-facing vs developer messages, panic recovery, and logger integration. Apply when using or adopting samber/oops, or when the codebase already imports github.com/samber/oops.","compatibility":"Designed for Claude Code or similar AI coding agents, and for projects using Golang."},"skills_sh_url":"https://skills.sh/samber/cc-skills-golang/golang-samber-oops"},"updatedAt":"2026-05-18T18:53:02.477Z"}}