vitessio/vitess · error

cannot Notify after starting to watch a config

Error message

cannot Notify after starting to watch a config

What it means

In go/viperutil/internal/sync, Viper.Notify registers a channel to receive change notifications and must be called before any Watch is established. Once watchingConfig is true the subscription set is frozen, so calling Notify afterwards panics to prevent a subscriber that would silently miss notifications. The doc comment on the method states this contract explicitly.

Source

Thrown at go/viperutil/internal/sync/sync.go:261

	defer v.m.Unlock()

	v.live.SetConfigFile(v.disk.ConfigFileUsed())

	return v.live.WriteConfig()
}

// Notify adds a subscription that this synced viper will attempt to notify on
// config changes, after the updated config has been copied over from disk to
// live.
//
// Analogous to signal.Notify, notifications are sent non-blocking, so users
// should account for this when consuming from the channel they've provided.
//
// This function must be called prior to setting up a Watch; it will panic if a
// a watch has already been established on this synced Viper.
func (v *Viper) Notify(ch chan<- struct{}) {
	if v.watchingConfig {
		panic("cannot Notify after starting to watch a config")
	}

	v.subscribers = append(v.subscribers, ch)
}

// AllSettings returns the current live settings.
func (v *Viper) AllSettings() map[string]any {
	v.m.Lock()
	defer v.m.Unlock()

	return v.live.AllSettings()
}

func (v *Viper) loadFromDisk() {
	v.m.Lock()
	defer v.m.Unlock()

	// Reset v.live so explicit Set calls don't win over what's just changed on

View on GitHub (pinned to 01a25a7d17)

Solutions

  1. Move all Notify calls before the Watch call — subscribe every consumer during initialization, then start watching last
  2. Give each package its own synced Viper instance so subscription and watching are independently ordered
  3. Wrap the shared setup in a single constructor that registers all subscribers and then starts the watch once

Example fix

// before
watcher.Watch(v) // starts watching
v.Notify(myCh)   // panics
// after
v.Notify(myCh)   // subscribe first
watcher.Watch(v) // then start watching
Defensive patterns

Strategy: validation

Validate before calling

// Expose a guarded subscribe on your wrapper
func Subscribe(v *sync.Viper, ch chan<- struct{}) error {
    if v.WatchingConfig() { // or track the flag yourself
        return errors.New("cannot Notify after Watch; subscribe during init")
    }
    v.Notify(ch)
    return nil
}

Try / catch

func() {
    defer func() {
        if r := recover(); r != nil && strings.Contains(fmt.Sprint(r), "cannot Notify after starting") {
            log.Fatalf("config subscriber registered too late: %v", r)
        }
    }()
    v.Notify(ch)
}()

Prevention

When it happens

Trigger: Calling syncedViper.Notify(ch) after Watch/Configure has already started watching the config — e.g. adding a subscriber in a later init or lazily on first use, while an earlier component already called Watch on the same synced Viper.

Common situations: Multiple packages share one synced Viper: package A sets up a watch during init, package B later tries to subscribe to changes and panics at startup or on first config access.

Related errors


AI-assisted analysis of vitessio/vitess@01a25a7d17 (2026-09-01). Data as JSON: /api/errors/5631cbcdd46094e1. Report an issue: GitHub.