uber-go/zap · error

missing Level

Error message

missing Level

What it means

Config.Build validates that a non-zero Level is configured before constructing the logger. Because AtomicLevel's zero value is not usable, zap rejects configurations where cfg.Level was never set, returning errors.New("missing Level"). This prevents silently building a logger with an uninitialized level.

Source

Thrown at config.go:251

		OutputPaths:      []string{"stderr"},
		ErrorOutputPaths: []string{"stderr"},
	}
}

// Build constructs a logger from the Config and Options.
func (cfg Config) Build(opts ...Option) (*Logger, error) {
	enc, err := cfg.buildEncoder()
	if err != nil {
		return nil, err
	}

	sink, errSink, err := cfg.openSinks()
	if err != nil {
		return nil, err
	}

	if cfg.Level == (AtomicLevel{}) {
		return nil, errors.New("missing Level")
	}

	log := New(
		zapcore.NewCore(enc, sink, cfg.Level),
		cfg.buildOptions(errSink)...,
	)
	if len(opts) > 0 {
		log = log.WithOptions(opts...)
	}
	return log, nil
}

func (cfg Config) buildOptions(errSink zapcore.WriteSyncer) []Option {
	opts := []Option{ErrorOutput(errSink)}

	if cfg.Development {
		opts = append(opts, Development())
	}

View on GitHub (pinned to bbd4ecbd87)

Solutions

  1. Set cfg.Level = zap.NewAtomicLevel() (or zap.NewAtomicLevelAt(zapcore.InfoLevel)) before calling Build
  2. Start from zap.NewProductionConfig() or zap.NewDevelopmentConfig() and override fields instead of building a zero-value Config
  3. If using an AtomicLevel shared with dynamic config, ensure it is initialized via zap.NewAtomicLevel before Build

Example fix

// before
cfg := zap.Config{Encoding: "json", OutputPaths: []string{"stderr"}}
logger, err := cfg.Build()
// after
cfg := zap.Config{Encoding: "json", OutputPaths: []string{"stderr"}}
cfg.Level = zap.NewAtomicLevelAt(zapcore.InfoLevel)
logger, err := cfg.Build()
Defensive patterns

Strategy: validation

Validate before calling

if cfg.Level == (zap.AtomicLevel{}) {
    cfg.Level = zap.NewAtomicLevelAt(zapcore.InfoLevel)
}
logger, err := cfg.Build()

Type guard

func hasLevel(c zap.Config) bool {
    return c.Level != (zap.AtomicLevel{})
}

Prevention

When it happens

Trigger: Calling cfg.Build() (or zap.New(...)-style construction via Config) on a zap.Config literal where the Level field was left as the zero-value AtomicLevel{} — e.g. zap.Config{Encoding:"json"} without cfg.Level = zap.NewAtomicLevel().

Common situations: Hand-rolled zap.Config structs instead of zap.NewProductionConfig()/NewDevelopmentConfig; copying a config but forgetting to re-set Level; dynamic config loading that omits the level key.

Related errors


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