{"record":{"id":"0e7fc25c197f7b70","repo":"siyuan-note/siyuan","slug":"failed-to-remove-storage-path-from-watcher-v","errorCode":null,"errorMessage":"failed to remove storage path from watcher: %v","messagePattern":"failed to remove storage path from watcher: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"kernel/plugin/api_storage.go","lineNumber":120,"sourceCode":"\t\tvar path string\n\t\tif len(call.Arguments) >= 1 && goja.IsString(call.Argument(0)) {\n\t\t\tpath = call.Argument(0).String()\n\t\t} else {\n\t\t\targErr = fmt.Errorf(\"path required\")\n\t\t}\n\n\t\trunErr := p.worker.Run(func(rt *goja.Runtime) (result any, err error) {\n\t\t\tif argErr != nil {\n\t\t\t\terr = argErr\n\t\t\t\treturn\n\t\t\t}\n\t\t\tabs, resolveErr := resolvePath(path)\n\t\t\tif resolveErr != nil {\n\t\t\t\terr = resolveErr\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif removeErr := p.removeStorageWatch(abs); removeErr != nil {\n\t\t\t\terr = fmt.Errorf(\"failed to remove storage path from watcher: %v\", removeErr)\n\t\t\t}\n\t\t\treturn\n\t\t}, func(rt *goja.Runtime, result any, err error) {\n\t\t\tif lo.IsNil(err) {\n\t\t\t\tif resolveErr := resolve(result); resolveErr != nil {\n\t\t\t\t\tlogging.LogErrorf(\"[plugin:%s] siyuan.storage.watcher.remove resolve: %v\", p.Name, resolveErr)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif rejectErr := reject(rt.NewGoError(err)); rejectErr != nil {\n\t\t\t\t\tlogging.LogErrorf(\"[plugin:%s] siyuan.storage.watcher.remove reject: %v\", p.Name, rejectErr)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t\tif runErr != nil {\n\t\t\tlogging.LogErrorf(\"[plugin:%s] siyuan.storage.watcher.remove worker run: %v\", p.Name, runErr)\n\t\t\tif rejectErr := reject(rt.NewGoError(runErr)); rejectErr != nil {\n\t\t\t\tlogging.LogErrorf(\"[plugin:%s] siyuan.storage.watcher.remove reject: %v\", p.Name, rejectErr)\n\t\t\t}","sourceCodeStart":102,"sourceCodeEnd":138,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/plugin/api_storage.go#L102-L138","documentation":"Thrown when p.removeStorageWatch(abs) returns an error after path resolution. The kernel tried to stop watching the given absolute path but the underlying watcher reported failure — most often because the path was never actually being watched, or the watcher had already been closed. The wrapped underlying error replaces %v.","triggerScenarios":"Calling watcher.remove on a path that was never added via watcher.add; calling remove twice for the same path; removing a path after the watcher was torn down during plugin unload.","commonSituations":"Plugin loses track of which paths it is watching and removes a stale entry; duplicate shutdown handlers fire remove more than once; the plugin reloaded and the in-memory watch set was reset but the JS side still thinks it is watching.","solutions":["Maintain a Set of currently-watched relative paths and only remove entries present in it.","Make remove idempotent: catch the rejection and ignore 'not watched' style errors.","Remove each path exactly once during shutdown."],"exampleFix":"// before\nawait siyuan.storage.watcher.remove('cache/state.json'); // may reject\n// after\ntry {\n  await siyuan.storage.watcher.remove('cache/state.json');\n} catch (e) { /* already removed or never added */ }\nwatchedPaths.delete('cache/state.json');","handlingStrategy":"fallback","validationCode":"const watched = new Set();\n// only remove paths we know we added:\nif (!watched.has(path)) return;","typeGuard":null,"tryCatchPattern":"try {\n  await siyuan.storage.watcher.remove(path);\n} catch (e) {\n  // already removed or never added: safe to ignore\n}\nwatched.delete(path);","preventionTips":["Make remove idempotent by catching the rejection.","Remove each path exactly once during shutdown."],"tags":["plugin-api","storage","watcher","fsnotify","idempotency"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}