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:sloglint

View on GitHub (pinned to dd8f660c0a)

Solutions

  1. Log with ce.Log() which promotes Fields and RealError to top-level attributes to see the full context
  2. Inspect the Context string to find the nebula subsystem that failed (config, handshake, lighthouse, etc.) and read the matching config docs
  3. If you control the code, populate RealError with the underlying error instead of only Context
  4. 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

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


AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03). Data as JSON: /api/errors/8aefb3df785ca889. Report an issue: GitHub.