siyuan-note/siyuan · error
failed to remove storage path from watcher
Error message
failed to remove storage path from watcher: %v
What it means
Wraps an error returned by the plugin's internal storage watcher when removeStorageWatch fails while servicing siyuan.storage.watcher.remove(path). The wrapped value (%v) is the underlying watcher error, typically a failure deregistering the path from the filesystem watcher (fsnotify) or an internal state inconsistency such as the watch entry not being removable.
Solutions
- Read the wrapped inner error (%v) in the rejected Promise to identify the watcher-level cause
- Ensure the path string passed to remove exactly matches the one passed to add (same relative form)
- Avoid calling watcher.remove concurrently or twice for the same path; serialize the calls
- If it occurs at plugin unload, skip removal errors — the watcher is disposed with the plugin anyway
Example fix
// before
await siyuan.storage.watcher.remove("./data/state.json"); // added as "data/state.json"
// after
await siyuan.storage.watcher.remove("data/state.json"); // same string as watcher.add Defensive patterns
Strategy: try-catch
Validate before calling
if (typeof path === "string" && path === lastAddedPath) { /* safe to remove */ } Type guard
const matchesAdded = (p) => typeof p === "string" && p === addedWatchPath;
Try / catch
try { await siyuan.storage.watcher.remove(path); } catch (e) { log("watcher remove failed:", e.message); /* non-fatal */ } Prevention
- Track added watch paths in a Set and remove only known entries
- Use the identical path string for add and remove (no ./ prefixes or case changes)
- Treat remove errors as non-fatal during plugin unload — the watcher is disposed anyway
- Avoid concurrent add/remove on the same path
When it happens
Trigger: Calling siyuan.storage.watcher.remove(path) with a path that resolves fine but whose watcher entry cannot be removed internally; concurrent add/remove races on the same path; watcher subsystem already closed or in a bad state.
Common situations: Plugin unloading code calling watcher.remove during teardown after the watcher was already disposed; removing the same path twice concurrently; watch was added through a different resolved path (e.g. different casing or './' prefix) than the one being removed.
Related errors
- failed to make directory
- failed to write file
- panic during siyuan.storage.get
- panic during siyuan.storage.put
- path and content required
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/0e7fc25c197f7b70.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/plugin/api_storage.go:120
var path string
if len(call.Arguments) >= 1 && goja.IsString(call.Argument(0)) {
path = call.Argument(0).String()
} else {
argErr = fmt.Errorf("path required")
}
runErr := p.worker.Run(func(rt *goja.Runtime) (result any, err error) {
if argErr != nil {
err = argErr
return
}
abs, resolveErr := resolvePath(path)
if resolveErr != nil {
err = resolveErr
return
}
if removeErr := p.removeStorageWatch(abs); removeErr != nil {
err = fmt.Errorf("failed to remove storage path from watcher: %v", removeErr)
}
return
}, func(rt *goja.Runtime, result any, err error) {
if lo.IsNil(err) {
if resolveErr := resolve(result); resolveErr != nil {
logging.LogErrorf("[plugin:%s] siyuan.storage.watcher.remove resolve: %v", p.Name, resolveErr)
}
} else {
if rejectErr := reject(rt.NewGoError(err)); rejectErr != nil {
logging.LogErrorf("[plugin:%s] siyuan.storage.watcher.remove reject: %v", p.Name, rejectErr)
}
}
})
if runErr != nil {
logging.LogErrorf("[plugin:%s] siyuan.storage.watcher.remove worker run: %v", p.Name, runErr)
if rejectErr := reject(rt.NewGoError(runErr)); rejectErr != nil {
logging.LogErrorf("[plugin:%s] siyuan.storage.watcher.remove reject: %v", p.Name, rejectErr)
}View on GitHub (pinned to 9f775e8a12)