microsoft/typescript-go · warning

no file watcher exists with ID %s

Error message

no file watcher exists with ID %s

What it means

UnwatchFiles (server.go:276-280) with the builtin watcher active: the requested project.WatcherID is not in the server's registered set (s.watchers), so there is nothing to remove. It is a caller-state error — the id was never registered or was already unwatched, and the error names the offending id.

Source

Thrown at internal/lsp/server.go:279

						Watchers: watchers,
					},
				},
			},
		},
	})
	if err != nil {
		return fmt.Errorf("failed to register file watcher: %w", err)
	}

	s.watchers.Add(id)
	return nil
}

// UnwatchFiles implements project.Client.
func (s *Server) UnwatchFiles(ctx context.Context, id project.WatcherID) error {
	if s.builtinWatcher != nil {
		if !s.watchers.Has(id) {
			return fmt.Errorf("no file watcher exists with ID %s", id)
		}
		if err := s.builtinWatcher.UnwatchFiles(string(id)); err != nil {
			return fmt.Errorf("failed to unregister file watcher: %w", err)
		}
		s.watchers.Delete(id)
		return nil
	}
	if s.watchers.Has(id) {
		_, err := sendClientRequest(ctx, s, lsproto.ClientUnregisterCapabilityInfo, &lsproto.UnregistrationParams{
			Unregisterations: []*lsproto.Unregistration{
				{
					Id:     string(id),
					Method: string(lsproto.MethodWorkspaceDidChangeWatchedFiles),
				},
			},
		})
		if err != nil {
			return fmt.Errorf("failed to unregister file watcher: %w", err)

View on GitHub (pinned to 1bcfa18d79)

Solutions

  1. Track watcher ids returned/created by your session layer and only unwatch ids you successfully registered (WatchFiles returned nil).
  2. Make cleanup idempotent in the caller: guard with your own 'registered' set or ignore this specific error during teardown.
  3. If hit on first unwatch, audit for a lost WatchFiles error that was swallowed earlier — registration may have failed and the id never entered s.watchers.

Example fix

// before
_ = client.UnwatchFiles(ctx, id)
// after: idempotent cleanup
if registered.Has(id) {
    if err := client.UnwatchFiles(ctx, id); err != nil { return err }
    registered.Delete(id)
}
Defensive patterns

Strategy: validation

Validate before calling

// caller-side registry of live watcher ids
var liveWatchers collections.SyncSet[project.WatcherID]

func register(ctx context.Context, c project.Client, id project.WatcherID, ws []*lsproto.FileSystemWatcher) error {
    if err := c.WatchFiles(ctx, id, ws); err != nil {
        return err
    }
    liveWatchers.Add(id)
    return nil
}

Try / catch

if err := client.UnwatchFiles(ctx, id); err != nil {
    if strings.HasPrefix(err.Error(), "no file watcher exists with ID") {
        // already gone: benign during idempotent teardown
        return nil
    }
    return err
}

Prevention

When it happens

Trigger: Calling project.Client.UnwatchFiles with an id that was never passed to WatchFiles, calling UnwatchFiles twice for the same id (the first call deletes it from the set), or a session tracking bug that loses which watcher ids are live.

Common situations: Retry logic that re-runs cleanup and hits the second UnwatchFiles; multiple projects sharing one client and double-releasing watchers; tests constructing arbitrary WatcherIDs.

Related errors


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