{"record":{"id":"4cea60b256c2abce","repo":"micro/go-micro","slug":"watcher-stopped","errorCode":null,"errorMessage":"watcher stopped","messagePattern":"watcher stopped","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"info","filePath":"config/loader/memory/memory.go","lineNumber":419,"sourceCode":"\n\t\tcs := &source.ChangeSet{\n\t\t\tData:      v.Bytes(),\n\t\t\tFormat:    w.reader.String(),\n\t\t\tSource:    \"memory\",\n\t\t\tTimestamp: time.Now(),\n\t\t}\n\t\tcs.Checksum = cs.Sum()\n\n\t\treturn &loader.Snapshot{\n\t\t\tChangeSet: cs,\n\t\t\tVersion:   w.getVersion(),\n\t\t}\n\t}\n\n\tfor {\n\t\tselect {\n\t\tcase <-w.exit:\n\t\t\treturn nil, errors.New(\"watcher stopped\")\n\n\t\tcase uv := <-w.updates:\n\t\t\tif uv.version <= w.getVersion() {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tv := uv.value\n\n\t\t\tw.version.Store(uv.version)\n\n\t\t\tif bytes.Equal(w.value.Bytes(), v.Bytes()) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treturn update(v), nil\n\t\t}\n\t}\n}","sourceCodeStart":401,"sourceCodeEnd":437,"githubUrl":"https://github.com/micro/go-micro/blob/24529f140421a11a33b6999ab7944f2021cfd69c/config/loader/memory/memory.go#L401-L437","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Treat errors.New(\"watcher stopped\") (compare err.Error()) as a normal shutdown signal and exit the watch loop cleanly.","Ensure only one goroutine owns the watcher lifecycle: the same owner should stop it and stop reading Next().","Do not call Next() after Stop(); restructure to select on a done channel alongside Next().","If you need watching again, create a new Watcher via loader.Watch instead of reusing the stopped one."],"exampleFix":"// before\nfor {\n    v, err := w.Next()\n    if err != nil { log.Error(err); return } // logs normal stops as errors\n}\n// after\nfor {\n    v, err := w.Next()\n    if err != nil {\n        if err.Error() == \"watcher stopped\" { return } // clean shutdown\n        log.Error(err); return\n    }\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"for {\n    v, err := w.Next()\n    if err != nil {\n        if err.Error() == \"watcher stopped\" {\n            return nil // clean shutdown, not a failure\n        }\n        return err\n    }\n    handle(v)\n}","preventionTips":["Have one owner goroutine for each watcher's lifecycle.","Compare the error string (or use errors.Is-style matching) to treat stops as normal.","Never call Next after Stop; use a done channel to coordinate.","Create a fresh Watcher via loader.Watch if watching must resume."],"tags":["config","watcher","lifecycle","go","shutdown"],"backgroundTag":"watcher-stopped","analyzedSha":"24529f140421a11a33b6999ab7944f2021cfd69c","analyzedAt":"2026-09-01T02:52:24.923Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}