microsoft/typescript-go · error
unable to open pipe: %w
Error message
unable to open pipe: %w
What it means
While starting the Linux fanotify backend, the wakeup pipe could not be created with pipe2(O_CLOEXEC|O_NONBLOCK). The raw errno is wrapped in the message. This is an OS-level failure of the backend's self-wakeup mechanism, before fanotify is even initialized.
Source
Thrown at internal/fswatch/fanotify_linux.go:223
// exercise the fallback path on kernels that natively support FAN_RENAME.
func newFanotifyBackend(noRename bool) *fanotifyBackend {
b := &fanotifyBackend{
pipeFDs: [2]int{-1, -1},
fanotifyFD: -1,
noRename: noRename,
subscriptions: map[fanotifyHandleKey][]*fanotifySubscription{},
endedSignal: make(chan struct{}),
readBuf: make([]byte, fanotifyBufferSize),
watchersTouched: make(map[*dirWatch]struct{}),
}
b.pipeWriteFD.Store(-1)
b.watcherBase.init(b)
return b
}
func (b *fanotifyBackend) start() error {
if err := unix.Pipe2(b.pipeFDs[:], unix.O_CLOEXEC|unix.O_NONBLOCK); err != nil {
return fmt.Errorf("unable to open pipe: %w", err)
}
b.pipeWriteFD.Store(int32(b.pipeFDs[1]))
defer func() {
b.closeFDs()
close(b.endedSignal)
}()
fd, err := unix.FanotifyInit(fanotifyInitFlags, unix.O_RDONLY|unix.O_CLOEXEC)
if err != nil {
return fmt.Errorf("unable to initialize fanotify: %w", err)
}
b.fanotifyFD = fd
pollfds := []unix.PollFd{
{Fd: int32(b.pipeFDs[0]), Events: unix.POLLIN},
{Fd: int32(b.fanotifyFD), Events: unix.POLLIN},
}
View on GitHub (pinned to 1bcfa18d79)
Solutions
- Raise the file-descriptor limit (ulimit -n / LimitNOFILE) and reduce concurrent open descriptors
- Retry after freeing descriptors — EMFILE is transient if something leaks fds; find the leak
- Relax the container seccomp/apparmor policy to allow pipe2
- Use a non-fanotify backend (e.g. inotify via fswatch.Inotify()) or polling if the environment cannot support it
Example fix
// before
w, err := fswatch.Default().WatchDirectory(dir, cb, fswatch.WithRecursive()) // fanotify start fails: pipe2 EMFILE
// after: raise RLIMIT_NOFILE (or use a lighter backend)
import "golang.org/x/sys/unix"
unix.Setrlimit(unix.RLIMIT_NOFILE, &unix.Rlimit{Cur: 65535, Max: 65535})
w, err := fswatch.Inotify().WatchDirectory(dir, cb, fswatch.WithRecursive()) Defensive patterns
Strategy: fallback
Validate before calling
// Before starting many watchers, confirm fd headroom.
var rl unix.Rlimit
if err := unix.Getrlimit(unix.RLIMIT_NOFILE, &rl); err == nil {
if rl.Cur < 1024 {
_ = unix.Setrlimit(unix.RLIMIT_NOFILE, &unix.Rlimit{Cur: 65535, Max: rl.Max})
}
} Type guard
func isPipeFailure(err error) bool {
return err != nil && strings.Contains(err.Error(), "unable to open pipe")
} Try / catch
w, err := watcher.WatchDirectory(dir, cb, opts...)
if err != nil && isPipeFailure(err) {
// EMFILE/ENOMEM at backend start: retry once, then degrade to polling.
time.Sleep(100 * time.Millisecond)
if w, err = watcher.WatchDirectory(dir, cb, opts...); err != nil {
return startPolling(dir, cb) // application-level poll fallback
}
} Prevention
- Set RLIMIT_NOFILE explicitly in long-lived servers instead of trusting inherited limits
- Cap the number of concurrent watchers and directory marks your app creates
- Fix fd leaks (unclosed watches/files) found via lsof before they surface as pipe2 EMFILE
When it happens
Trigger: Process hitting the open-file-descriptor limit (EMFILE/ENFILE) at watcher start; kernel memory pressure causing ENOMEM; seccomp/container policies blocking the pipe2 syscall.
Common situations: Editors/servers creating many watchers or otherwise holding many fds (check ulimit -n); heavily sandboxed containers (gVisor, strict Docker seccomp profiles); fork-bombed or memory-starved CI runners.
Related errors
- Error reading from fanotify: %w
- unable to initialize fanotify: %w
- unable to poll: %w
- fanotify_mark on '%s' failed: %w
- name_to_handle_at: %w
AI-assisted analysis of microsoft/typescript-go@1bcfa18d79 (2026-08-16).
Data as JSON: /api/errors/538db99f65dee294.
Report an issue: GitHub.