microsoft/typescript-go · error · ErrClientError

%w: failed to start CPU profile: %w

Error message

%w: failed to start CPU profile: %w

What it means

startCPUProfile forwarded the underlying profiler error, wrapped in ErrClientError. The wrapped error comes from the cpuProfiler: most commonly Go's pprof "CPU profiling already in progress" when a previous profile was never stopped, or a file-creation failure because dir does not exist or is not writable by the server process.

Source

Thrown at internal/api/session.go:874

	case string(MethodGetReferencesToSymbolInFile):
		return s.handleGetReferencesToSymbolInFile(ctx, parsed.(*GetReferencesToSymbolInFileParams))
	case string(MethodGetReferencedSymbolsForNode):
		return s.handleGetReferencedSymbolsForNode(ctx, parsed.(*GetReferencedSymbolsForNodeParams))
	case string(MethodGetSignatureUsages):
		return s.handleGetSignatureUsages(ctx, parsed.(*GetSignatureUsagesParams))
	case string(MethodGetCompletionsAtPosition):
		return s.handleGetCompletionsAtPosition(ctx, parsed.(*GetCompletionsAtPositionParams))
	default:
		return nil, fmt.Errorf("unknown method: %s", method)
	}
}

func (s *Session) handleStartCPUProfile(_ context.Context, params *ProfileParams) (any, error) {
	if params == nil || params.Dir == "" {
		return nil, fmt.Errorf("%w: dir is required", ErrClientError)
	}
	if err := s.cpuProfiler.StartCPUProfile(params.Dir); err != nil {
		return nil, fmt.Errorf("%w: failed to start CPU profile: %w", ErrClientError, err)
	}
	return nil, nil
}

func (s *Session) handleStopCPUProfile(_ context.Context) (*ProfileResult, error) {
	filePath, err := s.cpuProfiler.StopCPUProfile()
	if err != nil {
		return nil, fmt.Errorf("%w: failed to stop CPU profile: %w", ErrClientError, err)
	}
	return &ProfileResult{File: filePath}, nil
}

func (s *Session) handleSaveHeapProfile(_ context.Context, params *ProfileParams) (*ProfileResult, error) {
	if params == nil || params.Dir == "" {
		return nil, fmt.Errorf("%w: dir is required", ErrClientError)
	}
	filePath, err := pprof.SaveHeapProfile(params.Dir)
	if err != nil {

View on GitHub (pinned to 1bcfa18d79)

Solutions

  1. If a profile may already be running, call stopCPUProfile first (ignore its error) before starting again
  2. Ensure dir exists and is writable by the server process (mkdir -p, check ownership/permissions)
  3. Use an absolute path to avoid surprises from the server's working directory

Example fix

// before
start(dir) // may hit "already in progress"

// after
if _, err := stop(); err != nil { /* no active profile; ignore */ }
if err := start(dir); err != nil { return err }
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-flight: create the dir and prove it is writable by the server user.
if err := os.MkdirAll(dir, 0o755); err != nil { return err }
probe := filepath.Join(dir, ".writeprobe")
if err := os.WriteFile(probe, nil, 0o644); err != nil { return err }
os.Remove(probe)

Try / catch

if err != nil {
    msg := err.Error()
    switch {
    case strings.Contains(msg, "already in progress"):
        _, _ = session.HandleRequest(ctx, string(api.MethodStopCPUProfile), nil) // reset, then retry once
    case strings.Contains(msg, "permission") || strings.Contains(msg, "no such file"):
        return fmt.Errorf("profile dir not writable: %s", dir)
    }
    return err
}

Prevention

When it happens

Trigger: Calling startCPUProfile twice without an intervening stopCPUProfile; passing a dir that was never created; running the server under a user without write permission to dir; read-only container filesystems.

Common situations: Profiler lifecycle managed in separate code paths (start on demand, stop on shutdown) that can double-start; orchestrators mounting output dirs read-only; missing mkdir step in deployment scripts.

Related errors


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