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

  1. Read the wrapped inner error (%v) in the rejected Promise to identify the watcher-level cause
  2. Ensure the path string passed to remove exactly matches the one passed to add (same relative form)
  3. Avoid calling watcher.remove concurrently or twice for the same path; serialize the calls
  4. 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

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


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)