{"record":{"id":"cfef0f421ae8e3dd","repo":"microsoft/typescript-go","slug":"w-watched-directory-removed","errorCode":null,"errorMessage":"%w: watched directory removed","messagePattern":"%w: watched directory removed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/fswatch/fanotify_linux.go","lineNumber":572,"sourceCode":"\t\t\t// later events for the (now-moved) inodes would be reported\n\t\t\t// against stale paths. For FAN_MOVED_FROM that takes the\n\t\t\t// inode out of our watched tree the kernel mark on the\n\t\t\t// inode itself unfortunately leaks: fanotify has no\n\t\t\t// path-independent way to unmark and the destination is\n\t\t\t// outside everything we can resolve.\n\t\t\t// Self events may not have FAN_ONDIR set (like inotify).\n\t\t\tif isSelfMask || isDir {\n\t\t\t\tb.dropSubsForPathAndDescendantsLocked(path)\n\t\t\t} else {\n\t\t\t\tb.dropSubsForPathLocked(path)\n\t\t\t}\n\t\t\tw.events.remove(path)\n\t\t\ttouched = true\n\t\t\t// Root-of-watch deletion: the kernel has dropped the mark.\n\t\t\t// Surface ErrWatchTerminated alongside the delete so callers\n\t\t\t// know to clean up; no more events will arrive for w.\n\t\t\tif isSelfMask && path == w.dir {\n\t\t\t\tw.events.setError(fmt.Errorf(\"%w: watched directory removed\", ErrWatchTerminated))\n\t\t\t}\n\t\t}\n\t}\n\n\tif hasCreate {\n\t\tw.events.create(path)\n\t\tif isDir && w.recursive {\n\t\t\t_ = walkDir(w.physicalPath(path), true, func(p string, pIsDir bool) error {\n\t\t\t\tif !pIsDir {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\t_ = b.markDir(w, w.displayPath(p), p)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t}\n\t\ttouched = true\n\t}\n","sourceCodeStart":554,"sourceCodeEnd":590,"githubUrl":"https://github.com/microsoft/typescript-go/blob/1bcfa18d79a3be41772223d5c05dfe4480e614ff/internal/fswatch/fanotify_linux.go#L554-L590","documentation":"The fanotify backend detected that the root directory of a watch was itself deleted. The kernel drops the mark for a deleted root, so no further events can arrive; the backend sets a terminal error wrapping the sentinel fswatch.ErrWatchTerminated on that watch's event stream so subscribers clean up. This is an expected lifecycle signal, not a malfunction.","triggerScenarios":"Deleting (or renaming away) the exact directory passed to WatchDirectory — the delete self-event for the watch root triggers it; also unmounting the watched directory's filesystem.","commonSituations":"Build systems wiping temp/build directories; users deleting a project folder while an editor watches it; `rm -rf` of a watched root during cleanup scripts; git worktree removal.","solutions":["In the event callback, check for the error/terminated signal (errors.Is(err, fswatch.ErrWatchTerminated)) and Close the watch","If you must keep watching, subscribe to the parent and re-WatchDirectory when the directory is recreated","Design cleanup to remove files inside the root rather than the root itself when continuous watching is required"],"exampleFix":"// before: assumes the watch lives forever\nw, _ := fswatch.Default().WatchDirectory(dir, func(ev fswatch.Event) { ... })\n\n// after: handle terminal error, re-subscribe on recreation\nw, _ := fswatch.Default().WatchDirectory(dir, func(ev fswatch.Event) {\n    if errors.Is(ev.Err, fswatch.ErrWatchTerminated) {\n        w.Close()\n        waitForDirToExist(dir)          // e.g. via parent watch\n        w, _ = fswatch.Default().WatchDirectory(dir, cb) // re-subscribe\n    }\n})","handlingStrategy":"type-guard","validationCode":"// Optional: avoid the terminal error by watching a stable parent instead.\nparent := filepath.Dir(dir)\nif dir == parent { /* watching a filesystem root: no parent available */ }\nw, err := fswatch.Default().WatchDirectory(parent, func(ev fswatch.Event) {\n    if ev.HasDelete() && ev.Path == dir { handleRootRemoved(dir) }\n})","typeGuard":"func isWatchTerminated(err error) bool {\n    return err != nil && errors.Is(err, fswatch.ErrWatchTerminated)\n}","tryCatchPattern":"w, err := fswatch.Default().WatchDirectory(dir, cb)\nif err != nil { return err }\n// In the event callback / error channel:\nif isWatchTerminated(ev.Err) {\n    w.Close()                    // release remaining state\n    go resubscribeWhenRecreated(dir, cb) // optional: re-watch on recreation\n    return\n}","preventionTips":["Always handle ErrWatchTerminated in callbacks — deleted watch roots are a normal lifecycle event, not a bug","If deletion+recreation is common (build dirs), watch the parent and re-subscribe on create","Do not assume events keep flowing after this error; the kernel mark is gone"],"tags":["linux","fanotify","watch-terminated","lifecycle","fswatch"],"backgroundTag":null,"analyzedSha":"1bcfa18d79a3be41772223d5c05dfe4480e614ff","analyzedAt":"2026-08-16T02:12:00.115Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}