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
- Register each scheme exactly once, e.g. with sync.Once or in a single init() function.
- Check for the error and treat it as benign if the identical factory is already registered (skip re-registration).
- 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
- Register custom sinks once with sync.Once or in a single init()
- Give your sink a unique scheme namespaced to your organization
- In tests, register in TestMain rather than each test function
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
- can't register a sink factory for empty string
- must start with a letter
- %q is not a valid scheme: %v
- can't parse %q as a URL: %v
- missing Level
AI-assisted analysis of uber-go/zap@bbd4ecbd87 (2026-08-31).
Data as JSON: /api/errors/830337af0f5a1124.
Report an issue: GitHub.