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
- Serialize shutdown: only Close the watcher after start() has returned or use the watcher's own Close path rather than closing raw fds
- Treat this error as 'backend stopped': discard the watcher and re-create it if you still need watching
- 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
- Give every watcher exactly one owning goroutine for start/stop; never close fds from callbacks
- On shutdown errors, discard the whole watcher object rather than reusing it
- Log the wrapped errno — recurring EBADF indicates a teardown race; recurring ENOMEM indicates pressure
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
- statfs: %w
- Error reading from fanotify: %w
- unable to open pipe: %w
- unable to initialize fanotify: %w
- fanotify_mark on '%s' failed: %w
AI-assisted analysis of microsoft/typescript-go@1bcfa18d79 (2026-08-16).
Data as JSON: /api/errors/cf290fe0ae50653b.
Report an issue: GitHub.