micro/go-micro · info

watcher stopped

Error message

watcher stopped

What it means

The memory loader's watcher runs a loop selecting on its exit channel and the updates channel. When Stop() closes w.exit, the next Next() call returns "watcher stopped" to signal the watcher has been terminated and will deliver no further change sets. It is the documented end-of-life signal for a watcher, similar to io.EOF for streams.

Source

Thrown at config/loader/memory/memory.go:419

		cs := &source.ChangeSet{
			Data:      v.Bytes(),
			Format:    w.reader.String(),
			Source:    "memory",
			Timestamp: time.Now(),
		}
		cs.Checksum = cs.Sum()

		return &loader.Snapshot{
			ChangeSet: cs,
			Version:   w.getVersion(),
		}
	}

	for {
		select {
		case <-w.exit:
			return nil, errors.New("watcher stopped")

		case uv := <-w.updates:
			if uv.version <= w.getVersion() {
				continue
			}

			v := uv.value

			w.version.Store(uv.version)

			if bytes.Equal(w.value.Bytes(), v.Bytes()) {
				continue
			}

			return update(v), nil
		}
	}
}

View on GitHub (pinned to 24529f1404)

Solutions

  1. Treat errors.New("watcher stopped") (compare err.Error()) as a normal shutdown signal and exit the watch loop cleanly.
  2. Ensure only one goroutine owns the watcher lifecycle: the same owner should stop it and stop reading Next().
  3. Do not call Next() after Stop(); restructure to select on a done channel alongside Next().
  4. If you need watching again, create a new Watcher via loader.Watch instead of reusing the stopped one.

Example fix

// before
for {
    v, err := w.Next()
    if err != nil { log.Error(err); return } // logs normal stops as errors
}
// after
for {
    v, err := w.Next()
    if err != nil {
        if err.Error() == "watcher stopped" { return } // clean shutdown
        log.Error(err); return
    }
}
Defensive patterns

Strategy: try-catch

Try / catch

for {
    v, err := w.Next()
    if err != nil {
        if err.Error() == "watcher stopped" {
            return nil // clean shutdown, not a failure
        }
        return err
    }
    handle(v)
}

Prevention

When it happens

Trigger: Calling Next() on a loader.Watcher after watcher.Stop() was called (closing w.exit), or continuing to drain the watcher in a loop after another goroutine stopped it during shutdown.

Common situations: Graceful-shutdown code that stops config watchers while a background goroutine is still blocked in Next(); treating "watcher stopped" as a real failure and logging it as an error; double-stopping or restarting watchers on config reload.

Related errors


AI-assisted analysis of micro/go-micro@24529f1404 (2026-09-01). Data as JSON: /api/errors/4cea60b256c2abce. Report an issue: GitHub.