siyuan-note/siyuan · warning

fsnotify watcher not initialized

Error message

fsnotify watcher not initialized

What it means

Returned by removeStorageWatch when a plugin tries to stop watching a path but no fsnotify watcher exists (p.watcher == nil). The watcher is created lazily on the first addStorageWatch, so removing before ever adding, or after the watcher was closed, hits this guard.

Source

Thrown at kernel/plugin/plugin.go:800

		p.watcherDone = make(chan struct{})
		go p.startStorageWatch(p.watcher, p.watcherDone)
	}

	err = p.watcher.Add(path)
	return
}

// removeStorageWatch removes a path from the fsnotify watcher to stop watching for storage file/directory changes.
func (p *KernelPlugin) removeStorageWatch(path string) (err error) {
	if !isPluginFileWatchSupported() {
		return errPluginFileWatchUnsupported
	}

	p.watcherMu.Lock()
	defer p.watcherMu.Unlock()

	if p.watcher == nil {
		err = fmt.Errorf("fsnotify watcher not initialized")
		return
	}

	err = p.watcher.Remove(path)
	return
}

func (p *KernelPlugin) closeStorageWatcher() {
	p.watcherMu.Lock()
	watcher := p.watcher
	done := p.watcherDone
	p.watcher = nil
	p.watcherDone = nil
	p.watcherMu.Unlock()

	if watcher != nil {
		watcher.Close()
	}

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Track watch state in the plugin and only call remove for paths you successfully added.
  2. Ignore this specific error during teardown (it is benign if the watcher is already gone).
  3. Ensure add() resolved before registering the corresponding remove() in onunload.

Example fix

// before
onunload(() => { siyuan.storage.watcher.remove(path); });

// after
let added = false;
await siyuan.storage.watcher.add(path).then(() => { added = true; });
onunload(() => {
  if (!added) return;
  siyuan.storage.watcher.remove(path).catch(() => {});
});
Defensive patterns

Strategy: try-catch

Validate before calling

// track whether add() succeeded so remove() is only called when relevant
const watched = new Set<string>();
await siyuan.storage.watcher.add(path).then(() => watched.add(path));

Try / catch

try { await siyuan.storage.watcher.remove(path); } catch (e) { if (/not initialized/.test(String(e))) return; throw e; }

Prevention

When it happens

Trigger: Calling siyuan.storage.watcher.remove(path) before siyuan.storage.watcher.add(path), or after closeStorageWatcher has torn the watcher down during plugin unload.

Common situations: Plugin lifecycle bug: cleanup runs before setup, or onunload/duplicate-remove fires after the plugin context was cancelled and the watcher closed.

Related errors


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/9b732cdbbdfd09c6. Report an issue: GitHub.