uber-go/zap · error

sink factory already registered for scheme %q

Error message

sink factory already registered for scheme %q

What it means

RegisterSink stores one factory per normalized scheme; registering a second factory for the same scheme returns this error. Zap intentionally forbids overwriting existing sink factories to avoid silently changing behavior of already-constructed loggers.

Source

Thrown at sink.go:87

	// Infallible operation: the registry is empty, so we can't have a conflict.
	_ = sr.RegisterSink(schemeFile, sr.newFileSinkFromURL)
	return sr
}

// RegisterSink registers the given factory for the specific scheme.
func (sr *sinkRegistry) RegisterSink(scheme string, factory func(*url.URL) (Sink, error)) error {
	sr.mu.Lock()
	defer sr.mu.Unlock()

	if scheme == "" {
		return errors.New("can't register a sink factory for empty string")
	}
	normalized, err := normalizeScheme(scheme)
	if err != nil {
		return fmt.Errorf("%q is not a valid scheme: %v", scheme, err)
	}
	if _, ok := sr.factories[normalized]; ok {
		return fmt.Errorf("sink factory already registered for scheme %q", normalized)
	}
	sr.factories[normalized] = factory
	return nil
}

func (sr *sinkRegistry) newSink(rawURL string) (Sink, error) {
	// URL parsing doesn't work well for Windows paths such as `c:\log.txt`, as scheme is set to
	// the drive, and path is unset unless `c:/log.txt` is used.
	// To avoid Windows-specific URL handling, we instead check IsAbs to open as a file.
	// filepath.IsAbs is OS-specific, so IsAbs('c:/log.txt') is false outside of Windows.
	if filepath.IsAbs(rawURL) {
		return sr.newFileSinkFromPath(rawURL)
	}

	u, err := url.Parse(rawURL)
	if err != nil {
		return nil, fmt.Errorf("can't parse %q as a URL: %v", rawURL, err)
	}

View on GitHub (pinned to bbd4ecbd87)

Solutions

  1. Register each scheme exactly once, e.g. with sync.Once or in a single init() function.
  2. Check for the error and treat it as benign if the identical factory is already registered (skip re-registration).
  3. Use a unique scheme name for your custom sink (e.g. 'myco-kafka' instead of 'kafka').

Example fix

// before
func init() { zap.RegisterSink("kafka", newKafkaSink) }
func main() { zap.RegisterSink("kafka", newKafkaSink) // panics/error }
// after
var registerOnce sync.Once
func registerSink() {
    registerOnce.Do(func() { _ = zap.RegisterSink("kafka", newKafkaSink) })
}
Defensive patterns

Strategy: try-catch

Validate before calling

var registered sync.Map
func ensureRegistered(scheme string, f func(*url.URL) (zap.Sink, error)) error {
    if _, dup := registered.LoadOrStore(scheme, true); dup {
        return nil
    }
    return zap.RegisterSink(scheme, f)
}

Try / catch

if err := zap.RegisterSink(scheme, factory); err != nil {
    if strings.Contains(err.Error(), "already registered") {
        return nil // idempotent registration
    }
    return err
}

Prevention

When it happens

Trigger: Calling zap.RegisterSink("kafka", f) twice — typically once in package init() and once in main, or across two imported packages that both register the same scheme, or in tests that re-register per test case.

Common situations: Multiple libraries in the dependency graph each calling RegisterSink for their own scheme with the same name; running package tests where init registration runs per binary but manual registration is repeated; hot-reloading code that re-registers sinks.

Related errors


AI-assisted analysis of uber-go/zap@bbd4ecbd87 (2026-08-31). Data as JSON: /api/errors/830337af0f5a1124. Report an issue: GitHub.