{"record":{"id":"11fa0ce064139578","repo":"slackhq/nebula","slug":"s-v-w","errorCode":null,"errorMessage":"%s (%v): %w","messagePattern":"(.+?) \\((.+?)\\): %w","errorType":"exception","errorClass":"ContextualError","httpStatus":null,"severity":"info","filePath":"util/error.go","lineNumber":44,"sourceCode":"\t\treturn NewContextualError(msg, nil, err)\n\t}\n}\n\n// LogWithContextIfNeeded is a helper function to log an error line for an error or ContextualError\nfunc LogWithContextIfNeeded(msg string, err error, l *slog.Logger) {\n\tswitch v := err.(type) {\n\tcase *ContextualError:\n\t\tv.Log(l)\n\tdefault:\n\t\tl.Error(msg, \"error\", err)\n\t}\n}\n\nfunc (ce *ContextualError) Error() string {\n\tif ce.RealError == nil {\n\t\treturn ce.Context\n\t}\n\treturn fmt.Errorf(\"%s (%v): %w\", ce.Context, ce.Fields, ce.RealError).Error()\n}\n\nfunc (ce *ContextualError) Unwrap() error {\n\tif ce.RealError == nil {\n\t\treturn errors.New(ce.Context)\n\t}\n\treturn ce.RealError\n}\n\n// Log emits ce as a single error-level log line with Fields and RealError\n// promoted to top-level attributes, producing a flat shape callers can grep\n// or parse without walking into a nested object.\nfunc (ce *ContextualError) Log(l *slog.Logger) {\n\tattrs := make([]slog.Attr, 0, len(ce.Fields)+1)\n\tfor k, v := range ce.Fields {\n\t\tattrs = append(attrs, slog.Any(k, v))\n\t}\n\tif ce.RealError != nil {","sourceCodeStart":26,"sourceCodeEnd":62,"githubUrl":"https://github.com/slackhq/nebula/blob/dd8f660c0ac37903ec4080ca4d3c861ba9342ceb/util/error.go#L26-L62","documentation":"This is ContextualError.Error(): it renders context plus structured fields plus the wrapped real error as \"context (fields): realError\". If RealError is nil, only the Context string is shown. This type exists so callers can attach where/what context (and arbitrary fields) to a low-level error while keeping error unwrapping (errors.Is/As) functional via Unwrap.","triggerScenarios":"Any time a ContextualError's Error() method is called - e.g. printing or logging an error produced by the library's APIs (certificate parsing, handshake, firewall, etc.) that was wrapped with util.NewContextualError.","commonSituations":"Reading stack traces or logs where messages look like \"failed to load certificate (name: host-a): x509: ...\"; developers parsing the single-line string to extract cause; confusion when Fields are logged separately by LogWithContext.","solutions":["Read the message as: <context> (<fields>): <underlying cause> and address the underlying cause.","Use errors.Is/errors.As on the ContextualError (its Unwrap) to match root causes programmatically.","Call Unwrap() to get the RealError (or an errors.New of Context when RealError is nil)."],"exampleFix":"// before\nif strings.Contains(err.Error(), \"certificate\") { ... }\n// after\nvar ce *util.ContextualError\nif errors.As(err, &ce) {\n\tlog.Printf(\"%s %v\", ce.Context, ce.Fields)\n\troot := ce.Unwrap() // inspect the real cause\n}","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"func asContextualError(err error) (*util.ContextualError, bool) {\n\tvar ce *util.ContextualError\n\tif errors.As(err, &ce) {\n\t\treturn ce, true\n\t}\n\treturn nil, false\n}","tryCatchPattern":"var ce *util.ContextualError\nif errors.As(err, &ce) {\n\tlogger.Error(ce.Context, \"fields\", ce.Fields)\n\tcause := ce.Unwrap()\n\t// match root causes with errors.Is/As on cause\n} else {\n\tlogger.Error(err.Error())\n}","preventionTips":["Never string-match ContextualError text; use errors.As and its Fields field.","Always unwrap before retry/matching logic since Unwrap preserves the root cause.","Include ContextualError-aware formatting in shared logging helpers."],"tags":["error-handling","wrapping","logging"],"backgroundTag":"wrapped-error-context","analyzedSha":"dd8f660c0ac37903ec4080ca4d3c861ba9342ceb","analyzedAt":"2026-09-03T11:13:55.444Z","contentChangedAt":"2026-09-03T11:13:55.444Z","schemaVersion":2},"datasetVersion":"2026-09-10T17:17:09.494Z"}