microsoft/typescript-go · error

unable to poll: %w

Error message

unable to poll: %w

What it means

The fanotify event loop's poll(2) on {wake pipe, fanotify fd} returned an error other than EINTR (which is retried internally). This means the backend's core wait failed — typically EBADF from an fd closed concurrently, or ENOMEM under pressure. The backend stops and the error propagates out of start().

Source

Thrown at internal/fswatch/fanotify_linux.go:250

	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},
	}

	b.notifyStarted()

	for {
		_, err := unix.Poll(pollfds, 500)
		if err != nil {
			if errors.Is(err, unix.EINTR) {
				continue
			}
			return fmt.Errorf("unable to poll: %w", err)
		}
		if pollfds[0].Revents != 0 {
			break
		}
		if pollfds[1].Revents != 0 {
			if err := b.handleEvents(); err != nil {
				return err
			}
		}
	}

	return nil
}

func (b *fanotifyBackend) closeFDs() {
	b.mu.Lock()
	defer b.mu.Unlock()
	if b.pipeFDs[0] >= 0 {

View on GitHub (pinned to 1bcfa18d79)

Solutions

  1. Serialize shutdown: only Close the watcher after start() has returned or use the watcher's own Close path rather than closing raw fds
  2. Treat this error as 'backend stopped': discard the watcher and re-create it if you still need watching
  3. If ENOMEM-related, reduce concurrent watchers/buffers and retry with backoff

Example fix

// before
go watcher.Close() // races the running event loop's poll -> EBADF

// after: shut down through one path, only after start completed
err := backend.Start(ctx)
// ... later, from a single goroutine:
watcher.Close()
Defensive patterns

Strategy: retry

Type guard

func isPollFailure(err error) bool {
    return err != nil && strings.Contains(err.Error(), "unable to poll")
}

Try / catch

err := runWatchSession(ctx, dir, cb)
for retries := 0; isPollFailure(err) && retries < 3; retries++ {
    time.Sleep(time.Duration(retries+1) * 500 * time.Millisecond)
    err = runWatchSession(ctx, dir, cb) // rebuild watcher + backend from scratch
}

Prevention

When it happens

Trigger: Calling Close()/stop concurrently from another goroutine while the backend thread is inside poll (racing fd teardown); heavy memory pressure (ENOMEM); passing invalid poll fd state after an earlier partial failure.

Common situations: Watchers torn down from signal handlers or timers racing normal shutdown; tests that start and immediately close watchers; long-running daemons restarting watchers under load.

Related errors


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