slackhq/nebula · error
${ce.Context}
Error message
${ce.Context} What it means
ContextualError wraps a real error with a human-readable Context string and Fields map. Unwrap returns the wrapped RealError for errors.Is/As chains; however, if RealError is nil, it fabricates errors.New(ce.Context), so the message seen is just the context text (rendered by Error() as "<Context> (<fields>): <real>"). This keeps context-only errors (validation before any underlying failure) unwrap-safe.
Source
Thrown at util/error.go:49
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 {
attrs = append(attrs, slog.Any("error", ce.RealError))
}
// LogAttrs is intentional: attrs is built from a map[string]any so it has
// no pair-form equivalent.
//nolint:sloglintView on GitHub (pinned to dd8f660c0a)
Solutions
- Log with ce.Log() which promotes Fields and RealError to top-level attributes to see the full context
- Inspect the Context string to find the nebula subsystem that failed (config, handshake, lighthouse, etc.) and read the matching config docs
- If you control the code, populate RealError with the underlying error instead of only Context
- Use errors.As to extract *ContextualError and read ce.Fields for structured details
Example fix
// before
return NewContextualError("failed to get host info", m, nil)
// after
return NewContextualError("failed to get host info", m, underlyingErr) Defensive patterns
Strategy: try-catch
Validate before calling
var ce *util.ContextualError
if errors.As(err, &ce) && ce.RealError == nil {
// context-only error: no underlying cause will surface via Unwrap
} Type guard
func hasRealError(ce *ContextualError) bool { return ce != nil && ce.RealError != nil } Try / catch
var ce *ContextualError
if errors.As(err, &ce) {
ce.Log(l) // flat log line with Fields + RealError attributes
return ce.RealError
}
return err Prevention
- Always pass the underlying error as RealError, not nil
- Use ce.Log() instead of err.Error() for full structured detail
- Match on Context strings when grepping nebula logs
- Avoid string-comparing Unwrapped errors when Context-only wrapping is possible
When it happens
Trigger: Any code path returning a ContextualError whose RealError field is nil, followed by errors.Is/errors.As/unwrap-based handling; the reported message `${ce.Context}` is the Context string, e.g. "while loading config".
Common situations: Nebula failing during config parsing or handshake setup where ContextualError is constructed with only Context and Fields; log scrapers/alerting matching on the context text while the real cause is absent.
Related errors
- failed to cast command
- config '%s.%s': %w
- calculated_remotes entry: %w
- ErrTruncatedPEMBlock
- Empty configuration
AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03).
Data as JSON: /api/errors/8aefb3df785ca889.
Report an issue: GitHub.