microsoft/typescript-go · error
fswatch: failed to watch directory %q: %w
Error message
fswatch: failed to watch directory %q: %w
What it means
Error from the fallback watcher's per-request re-subscribe path. When the primary (fanotify) batch fails with ErrFilesystemUnsupported, each request is retried individually; this error means that for request.Dir both the fanotify primary and the inotify secondary failed. The message names the directory and wraps the underlying error, and every watch already created for earlier requests in the batch is rolled back before returning.
Source
Thrown at internal/fswatch/watcher.go:284
watches, err := w.primary.WatchDirectories(requests)
if err == nil || !errors.Is(err, ErrFilesystemUnsupported) {
return watches, err
}
watches = make([]Watch, 0, len(requests))
rollback := func() {
for i := len(watches) - 1; i >= 0; i-- {
_ = watches[i].Close()
}
}
for _, request := range requests {
watch, err := w.primary.WatchDirectory(request.Dir, request.Callback, request.Options...)
if errors.Is(err, ErrFilesystemUnsupported) {
watch, err = w.secondary.WatchDirectory(request.Dir, request.Callback, request.Options...)
}
if err != nil {
rollback()
return nil, fmt.Errorf("fswatch: failed to watch directory %q: %w", request.Dir, err)
}
watches = append(watches, watch)
}
return watches, nil
}
func (w *fallbackWatcher) WatchFile(path string, fn WatchCallback) (Watch, error) {
watch, err := w.primary.WatchFile(path, fn)
if errors.Is(err, ErrFilesystemUnsupported) {
return w.secondary.WatchFile(path, fn)
}
return watch, err
}
func (w *fallbackWatcher) unexported() {}
// watcher is the concrete implementation of [Watcher]. Each platform
// watcher is a package-level *watcher whose factory is set by theView on GitHub (pinned to 1bcfa18d79)
Solutions
- Unwrap the error to identify the cause: ENOSPC means raise fs.inotify.max_user_watches and fs.inotify.max_user_instances via sysctl
- Verify the directory exists at subscribe time; re-check and retry if it is created asynchronously
- Trim recursive watch scope to reduce kernel watch consumption
- Move the tree off the unsupported filesystem, or run outside the container
Example fix
// before
watches, err := fswatch.Fanotify().WatchDirectories(reqs)
if err != nil { return err }
// after
watches, err := fswatch.Fanotify().WatchDirectories(reqs)
if err != nil {
var errno syscall.Errno
if errors.As(err, &errno) && errno == unix.ENOSPC {
// raise fs.inotify.max_user_watches, then retry
}
return err
} Defensive patterns
Strategy: try-catch
Validate before calling
if _, err := os.Stat(dir); err != nil {
// create or wait for the directory before batch-subscribing
} Try / catch
watches, err := fswatch.Fanotify().WatchDirectories(reqs)
if err != nil {
var errno syscall.Errno
if errors.As(err, &errno) {
switch errno {
case unix.ENOSPC:
// raise fs.inotify.max_user_watches / max_user_instances, retry
case unix.EACCES:
// fix permissions on dir
case unix.ENOENT:
// recreate dir, retry
}
}
} Prevention
- Raise inotify sysctl limits before watching large trees in containers
- Verify directories exist before batch subscribe
- Keep batches small enough that rollback is cheap
- Unwrap batch errors: the fallback already retried; the secondary error is the real cause
When it happens
Trigger: Fanotify() watching a filesystem it cannot handle (overlayfs, virtiofs, FUSE) where inotify then also fails: the directory disappeared, inotify instance or watch limits were hit (ENOSPC from fs.inotify.max_user_watches), or permissions deny the watch.
Common situations: Deep trees inside Docker containers where fanotify is unsupported and the inotify watch limit is also exceeded. Directories removed between the primary batch attempt and the secondary retry.
Related errors
- name_to_handle_at: %w
- %w: %w
- fswatch: watcher backend unsupported on this filesystem
- unable to open pipe: %w
- unable to initialize fanotify: %w
AI-assisted analysis of microsoft/typescript-go@1bcfa18d79 (2026-08-16).
Data as JSON: /api/errors/1ddf5e136a7c5fa9.
Report an issue: GitHub.