{"record":{"id":"cf290fe0ae50653b","repo":"microsoft/typescript-go","slug":"unable-to-poll-w","errorCode":null,"errorMessage":"unable to poll: %w","messagePattern":"unable to poll: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/fswatch/fanotify_linux.go","lineNumber":250,"sourceCode":"\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to initialize fanotify: %w\", err)\n\t}\n\tb.fanotifyFD = fd\n\n\tpollfds := []unix.PollFd{\n\t\t{Fd: int32(b.pipeFDs[0]), Events: unix.POLLIN},\n\t\t{Fd: int32(b.fanotifyFD), Events: unix.POLLIN},\n\t}\n\n\tb.notifyStarted()\n\n\tfor {\n\t\t_, err := unix.Poll(pollfds, 500)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, unix.EINTR) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"unable to poll: %w\", err)\n\t\t}\n\t\tif pollfds[0].Revents != 0 {\n\t\t\tbreak\n\t\t}\n\t\tif pollfds[1].Revents != 0 {\n\t\t\tif err := b.handleEvents(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (b *fanotifyBackend) closeFDs() {\n\tb.mu.Lock()\n\tdefer b.mu.Unlock()\n\tif b.pipeFDs[0] >= 0 {","sourceCodeStart":232,"sourceCodeEnd":268,"githubUrl":"https://github.com/microsoft/typescript-go/blob/1bcfa18d79a3be41772223d5c05dfe4480e614ff/internal/fswatch/fanotify_linux.go#L232-L268","documentation":"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().","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\ngo watcher.Close() // races the running event loop's poll -> EBADF\n\n// after: shut down through one path, only after start completed\nerr := backend.Start(ctx)\n// ... later, from a single goroutine:\nwatcher.Close()","handlingStrategy":"retry","validationCode":null,"typeGuard":"func isPollFailure(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"unable to poll\")\n}","tryCatchPattern":"err := runWatchSession(ctx, dir, cb)\nfor retries := 0; isPollFailure(err) && retries < 3; retries++ {\n    time.Sleep(time.Duration(retries+1) * 500 * time.Millisecond)\n    err = runWatchSession(ctx, dir, cb) // rebuild watcher + backend from scratch\n}","preventionTips":["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"],"tags":["linux","fanotify","race-condition","fswatch","poll"],"backgroundTag":null,"analyzedSha":"1bcfa18d79a3be41772223d5c05dfe4480e614ff","analyzedAt":"2026-08-16T02:12:00.115Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}