slackhq/nebula · info · ContextualError
%s (%v): %w
Error message
%s (%v): %w
What it means
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.
Source
Thrown at util/error.go:44
return NewContextualError(msg, nil, err)
}
}
// LogWithContextIfNeeded is a helper function to log an error line for an error or ContextualError
func LogWithContextIfNeeded(msg string, err error, l *slog.Logger) {
switch v := err.(type) {
case *ContextualError:
v.Log(l)
default:
l.Error(msg, "error", err)
}
}
func (ce *ContextualError) Error() string {
if ce.RealError == nil {
return ce.Context
}
return fmt.Errorf("%s (%v): %w", ce.Context, ce.Fields, ce.RealError).Error()
}
func (ce *ContextualError) Unwrap() error {
if ce.RealError == nil {
return errors.New(ce.Context)
}
return ce.RealError
}
// Log emits ce as a single error-level log line with Fields and RealError
// promoted to top-level attributes, producing a flat shape callers can grep
// or parse without walking into a nested object.
func (ce *ContextualError) Log(l *slog.Logger) {
attrs := make([]slog.Attr, 0, len(ce.Fields)+1)
for k, v := range ce.Fields {
attrs = append(attrs, slog.Any(k, v))
}
if ce.RealError != nil {View on GitHub (pinned to dd8f660c0a)
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).
Example fix
// before
if strings.Contains(err.Error(), "certificate") { ... }
// after
var ce *util.ContextualError
if errors.As(err, &ce) {
log.Printf("%s %v", ce.Context, ce.Fields)
root := ce.Unwrap() // inspect the real cause
} Defensive patterns
Strategy: type-guard
Type guard
func asContextualError(err error) (*util.ContextualError, bool) {
var ce *util.ContextualError
if errors.As(err, &ce) {
return ce, true
}
return nil, false
} Try / catch
var ce *util.ContextualError
if errors.As(err, &ce) {
logger.Error(ce.Context, "fields", ce.Fields)
cause := ce.Unwrap()
// match root causes with errors.Is/As on cause
} else {
logger.Error(err.Error())
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03).
Data as JSON: /api/errors/11fa0ce064139578.
Report an issue: GitHub.