{"record":{"id":"2d1a5876973b8857","repo":"microsoft/typescript-go","slug":"failed-to-unregister-file-watcher-w","errorCode":null,"errorMessage":"failed to unregister file watcher: %w","messagePattern":"failed to unregister file watcher: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/lsp/server.go","lineNumber":282,"sourceCode":"\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to register file watcher: %w\", err)\n\t}\n\n\ts.watchers.Add(id)\n\treturn nil\n}\n\n// UnwatchFiles implements project.Client.\nfunc (s *Server) UnwatchFiles(ctx context.Context, id project.WatcherID) error {\n\tif s.builtinWatcher != nil {\n\t\tif !s.watchers.Has(id) {\n\t\t\treturn fmt.Errorf(\"no file watcher exists with ID %s\", id)\n\t\t}\n\t\tif err := s.builtinWatcher.UnwatchFiles(string(id)); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to unregister file watcher: %w\", err)\n\t\t}\n\t\ts.watchers.Delete(id)\n\t\treturn nil\n\t}\n\tif s.watchers.Has(id) {\n\t\t_, err := sendClientRequest(ctx, s, lsproto.ClientUnregisterCapabilityInfo, &lsproto.UnregistrationParams{\n\t\t\tUnregisterations: []*lsproto.Unregistration{\n\t\t\t\t{\n\t\t\t\t\tId:     string(id),\n\t\t\t\t\tMethod: string(lsproto.MethodWorkspaceDidChangeWatchedFiles),\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to unregister file watcher: %w\", err)\n\t\t}\n\n\t\ts.watchers.Delete(id)","sourceCodeStart":264,"sourceCodeEnd":300,"githubUrl":"https://github.com/microsoft/typescript-go/blob/1bcfa18d79a3be41772223d5c05dfe4480e614ff/internal/lsp/server.go#L264-L300","documentation":"UnwatchFiles (server.go:281-283) with the builtin watcher active: the id was registered, but the underlying lspwatcher backend failed to remove the OS-level watch; the error wraps the backend failure with %w. Set deletion is skipped on this path, so the id remains registered — the caller can retry.","triggerScenarios":"The OS watch handle is already gone (directory deleted), backend close errors, or a backend-specific failure while unwatching the pattern set registered under that id string.","commonSituations":"Projects whose watched directories are deleted or unmounted before cleanup; shutdown races where the watcher is closing concurrently; resource exhaustion making teardown fail.","solutions":["Retry UnwatchFiles once — because s.watchers still contains the id on this failure path, a retry is safe and often succeeds after transient backend errors.","If the watched tree was deleted, the error is usually benign during teardown; log and continue.","Check the wrapped backend error for actionable causes (handle invalid, path gone) before retrying."],"exampleFix":"// before\nif err := client.UnwatchFiles(ctx, id); err != nil { return err }\n// after: tolerate failures for vanished paths during teardown\nif err := client.UnwatchFiles(ctx, id); err != nil && !errors.Is(err, fs.ErrNotExist) {\n    log.Printf(\"unwatch %s: %v\", id, err)\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := client.UnwatchFiles(ctx, id); err != nil {\n    if strings.Contains(err.Error(), \"failed to unregister file watcher\") {\n        // id remains registered on this path; one retry is safe\n        if err2 := client.UnwatchFiles(ctx, id); err2 != nil {\n            log.Printf(\"unwatch %s failed twice: %v / %v\", id, err, err2)\n        }\n    } else {\n        return err\n    }\n}","preventionTips":["On unregister failure the id stays in s.watchers, so retries are meaningful — build cleanup to allow one retry.","During teardown after directory deletion, log and continue rather than propagating.","Inspect the wrapped backend error before deciding to retry."],"tags":["lsp","file-watcher","retry","teardown"],"backgroundTag":null,"analyzedSha":"1bcfa18d79a3be41772223d5c05dfe4480e614ff","analyzedAt":"2026-08-16T02:12:00.115Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}