microsoft/typescript-go · error · dirWatchError
fanotify_mark on '%s' failed: %w
Error message
fanotify_mark on '%s' failed: %w
What it means
Adding a non-recursive watch failed at its single fanotify_mark(FAN_MARK_ADD) call on the watch directory; the raw errno is wrapped and the failure is reported as a dirWatchError naming the offending directory. Marks consume kernel quota and require supported filesystems, so the errno usually points at quota, permissions, or filesystem support.
Source
Thrown at internal/fswatch/fanotify_linux.go:326
// attached for the life of the process, but since
// markDir below will Add the real mask with the same
// flags the kernel just merges them. The probe is the
// only failure path we explicitly retry.
for {
rmErr := unix.FanotifyMark(b.fanotifyFD, unix.FAN_MARK_REMOVE|unix.FAN_MARK_ONLYDIR, fanotifyMarkMaskRename, unix.AT_FDCWD, w.physicalDir)
if rmErr == nil || !errors.Is(rmErr, unix.EINTR) {
break
}
}
case errors.Is(err, unix.EINVAL), errors.Is(err, unix.EOPNOTSUPP):
b.markMask = fanotifyMarkMaskMovedFromTo
}
}
}
if !w.recursive {
if err := b.markDir(w, w.dir, w.physicalDir); err != nil {
return &dirWatchError{
err: fmt.Errorf("fanotify_mark on '%s' failed: %w", w.dir, err),
dirWatch: w,
}
}
return nil
}
if err := walkDir(w.physicalDir, true, func(watchPath string, isDir bool) error {
if !isDir {
return nil
}
path := w.displayPath(watchPath)
if err := b.markDir(w, path, watchPath); err != nil {
return &dirWatchError{
err: fmt.Errorf("fanotify_mark on '%s' failed: %w", path, err),
dirWatch: w,
}
}
return nil
}); err != nil {View on GitHub (pinned to 1bcfa18d79)
Solutions
- Raise fs.fanotify.max_user_marks (sysctl) if errno is ENOSPC, or watch fewer directories (narrow includes, ignore node_modules)
- Check errors.Is(err, fswatch.ErrFilesystemUnsupported) and switch that path to inotify or polling
- Grant CAP_SYS_ADMIN / run privileged if the errno is EPERM on your kernel
- Watch the closest existing ancestor and re-subscribe when the target directory is created instead of racing deletions
Example fix
# before: ENOSPC after ~8k marks (default max_user_marks=8192)
sudo sysctl -w fs.fanotify.max_user_marks=65536
# and/or prune the tree in Go:
w, err := fswatch.Default().WatchDirectory(dir, cb, fswatch.WithRecursive(), fswatch.WithIgnore(func(p string) bool { return strings.Contains(p, "node_modules") })) Defensive patterns
Strategy: fallback
Validate before calling
// Pre-flight the sysctl budget for mark-heavy watches.
func markBudget() (cur int64, err error) {
b, err := os.ReadFile("/proc/sys/fs/fanotify/max_user_marks")
if err != nil { return 0, err }
return strconv.ParseInt(strings.TrimSpace(string(b)), 10, 64)
} Type guard
func isMarkFailure(err error) bool {
var dw *fswatch.DirWatchError // if exposed; otherwise match on message
return err != nil && strings.Contains(err.Error(), "fanotify_mark")
}
func isUnsupportedFS(err error) bool {
return err != nil && errors.Is(err, fswatch.ErrFilesystemUnsupported)
} Try / catch
w, err := fswatch.Default().WatchDirectory(dir, cb, opts...)
if err != nil {
switch {
case isUnsupportedFS(err):
w, err = fswatch.Inotify().WatchDirectory(dir, cb, opts...) // different fs access path
case isMarkFailure(err):
sysctlSet("fs.fanotify.max_user_marks", "65536") // or prompt the user to
w, err = fswatch.Default().WatchDirectory(dir, cb, opts...)
}
}
if err != nil { return err } Prevention
- Set fs.fanotify.max_user_marks generously on dev machines/CI hosts that watch big trees
- Prefer one recursive watch with ignore filters over many single-directory watches
- Remember marks persist per uid across processes: unrelated fanotify users consume the same quota
When it happens
Trigger: Exceeding /proc/sys/fs/fanotify/max_user_marks (ENOSPC) when watching many directories; EPERM where the mark flags need privilege; EINVAL/EOPNOTSUPP/ENODEV on filesystems fanotify FID mode cannot handle (some FUSE, overlayfs, fuseblk NTFS — the latter wrapped as ErrFilesystemUnsupported); target deleted between stat and mark.
Common situations: Recursive-in-userspace watches over huge monorepos exhausting mark quota; dev containers on overlayfs/virtiofs bind mounts; network filesystems; missing CAP_SYS_ADMIN on older kernels.
Related errors
- unable to open pipe: %w
- unable to initialize fanotify: %w
- unable to poll: %w
- name_to_handle_at: %w
- statfs: %w
AI-assisted analysis of microsoft/typescript-go@1bcfa18d79 (2026-08-16).
Data as JSON: /api/errors/fe028ca7ddec800b.
Report an issue: GitHub.