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

  1. Read the message as: <context> (<fields>): <underlying cause> and address the underlying cause.
  2. Use errors.Is/errors.As on the ContextualError (its Unwrap) to match root causes programmatically.
  3. 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

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.