microsoft/typescript-go · error

%w: watched directory removed

Error message

%w: watched directory removed

What it means

Terminal error delivered on the WatchCallback when the root directory of a kqueue watch (macOS/BSD) is deleted or renamed away. handleFileEvent fires this after it closes the root fd and every descendant fd, because no further events can ever fire for that dirWatch. The message wraps ErrWatchTerminated, so errors.Is(err, fswatch.ErrWatchTerminated) matches.

Source

Thrown at internal/fswatch/kqueue.go:331

				// kernels (OpenBSD in particular) deliver only the
				// parent's NOTE_DELETE/NOTE_RENAME and never fire
				// NOTE_DELETE on the children; without this cleanup,
				// modifying a file inside the moved tree later
				// surfaces an event against the descendant's stale
				// (pre-rename) path. We also emit a delete for each
				// descendant we close, so callers don't miss those
				// removals if the kernel didn't fire per-child events.
				// (When the kernel does fire them, our follow-up
				// handleFileEvent finds the fd already gone and is a
				// no-op, so events.create's coalescing handles dups.)
				if entry.isDir {
					b.closeDescendantFDsLocked(sub.dirWatch, sub.entries, sub.path)
				}
				removeEntryAndDescendants(sub.entries, sub.path)
				// Root-of-watch deletion: no more events can fire
				// for this dirWatch. Tell the caller.
				if sub.path == sub.dirWatch.dir {
					sub.dirWatch.events.setError(fmt.Errorf("%w: watched directory removed", ErrWatchTerminated))
				}
			}
		}
		if !recreated {
			delete(b.subsByPath, entry.path)
		}
		return
	}

	for _, sub := range subs {
		touched[sub.dirWatch] = struct{}{}
		if fflags&(unix.NOTE_WRITE|unix.NOTE_ATTRIB|unix.NOTE_EXTEND) != 0 {
			sub.dirWatch.events.update(sub.path)
		}
	}
}

// closeDescendantFDsLocked closes every fd attached to an entry whose

View on GitHub (pinned to 1bcfa18d79)

Solutions

  1. Handle ErrWatchTerminated in the callback and call Close() on the Watch to release remaining state
  2. Re-subscribe with WatchDirectory once the directory has been recreated; poll for its existence first
  3. If the directory is deleted and recreated routinely, watch its stable parent instead and filter events for the child path
  4. Layer a polling fallback for paths whose delete/recreate churn makes re-watching unreliable

Example fix

// before
w.WatchDirectory(dir, func(events []fswatch.Event, err error) {
    if err != nil {
        log.Fatal(err) // crashes when the watch root is deleted
    }
    apply(events)
})

// after
watch, err := w.WatchDirectory(dir, func(events []fswatch.Event, err error) {
    if err != nil {
        if errors.Is(err, fswatch.ErrWatchTerminated) {
            watch.Close()
            go resubscribeWhenDirExists(dir) // poll, then WatchDirectory again
        }
        return
    }
    apply(events)
})
Defensive patterns

Strategy: try-catch

Type guard

func isWatchTerminated(err error) bool {
    return errors.Is(err, fswatch.ErrWatchTerminated)
}

Try / catch

watch, _ := w.WatchDirectory(dir, func(events []fswatch.Event, err error) {
    if errors.Is(err, fswatch.ErrWatchTerminated) {
        watch.Close()
        resubscribeWhenDirExists(dir)
        return
    }
    // normal processing
})

Prevention

When it happens

Trigger: Call WatchDirectory on the Kqueue() watcher, then delete (rm -rf) or rename (mv) the watched directory itself. Also delivered for WatchFile when the parent directory of the file is removed. Deleting only a child of the watch root does not fire it; the watch survives.

Common situations: Build scripts that run 'rm -rf out && mkdir out'. Test harnesses removing temp directories. Git branch switches or git clean replacing directories. Watching directories that a package manager recreates during install.

Related errors


AI-assisted analysis of microsoft/typescript-go@1bcfa18d79 (2026-08-16). Data as JSON: /api/errors/6094092c990316aa. Report an issue: GitHub.