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

  1. Raise the file-descriptor limit (ulimit -n / LimitNOFILE) and reduce concurrent open descriptors
  2. Retry after freeing descriptors — EMFILE is transient if something leaks fds; find the leak
  3. Relax the container seccomp/apparmor policy to allow pipe2
  4. 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

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


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