gastownhall/beads · warning

procid: close process handle: %w

Error message

procid: close process handle: %w

What it means

Handle.Close calls windows.CloseHandle on the stored process handle and wraps any failure. This almost always means the handle value is invalid — typically a double-Close or use-after-Close, since Close sets h.process = 0 afterward, which normally makes repeat Close a no-op.

Source

Thrown at internal/procid/procid_windows.go:101

	if err := windows.TerminateProcess(h.process, 1); err != nil {
		if _, exitedErr := tokenForProcess(h.process); errors.Is(exitedErr, errProcessExited) {
			return nil
		}
		return fmt.Errorf("procid: terminate process: %w", err)
	}
	return nil
}

func (h *Handle) Kill() error { return h.Signal(os.Kill) }

func (h *Handle) Close() error {
	if h.process == 0 {
		return nil
	}
	err := windows.CloseHandle(h.process)
	h.process = 0
	if err != nil {
		return fmt.Errorf("procid: close process handle: %w", err)
	}
	return nil
}

func (h *Handle) verify() error {
	current, err := tokenForProcess(h.process)
	if err != nil {
		return err
	}
	if current != h.token {
		return fmt.Errorf("procid: process no longer matches token")
	}
	return nil
}

func openProcess(pid int, access uint32) (windows.Handle, error) {
	return windows.OpenProcess(access, false, uint32(pid))
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Guard Close with sync.Once or a mutex so it runs exactly once
  2. Never copy the *Handle by value; always pass *procid.Handle by pointer
  3. Ignore or downgrade INVALID_HANDLE_VALUE-class errors on idempotent shutdown paths
  4. Always obtain Handle via procid.Open; don't construct one manually

Example fix

// before
var closeOnce sync.Once
defer h.Close()
defer h.Close() // second call races
// after
var closeOnce sync.Once
closeFn := func() { _ = h.Close() }
defer closeOnce.Do(closeFn)
defer closeOnce.Do(closeFn)
Defensive patterns

Strategy: try-catch

Validate before calling

// Guard against double close at call sites
var closed bool
func shutdown(h *procid.Handle) error {
    if closed { return nil }
    closed = true
    return h.Close()
}

Type guard

func safeClose(h *procid.Handle) error {
    if h == nil {
        return nil
    }
    return h.Close() // Close itself is idempotent when h.process == 0
}

Try / catch

if err := h.Close(); err != nil {
    // invalid handle on close is rarely actionable; log and continue
    log.Printf("close process handle: %v", err)
}

Prevention

When it happens

Trigger: Calling Close twice concurrently (race: both see h.process != 0); closing a Handle whose underlying handle was invalidated externally; passing a zero-value Handle built outside Open.

Common situations: Concurrent shutdown paths both calling Close/deferred Close; copying the Handle struct by value so two copies own the same raw handle.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/700a5446d18d2320. Report an issue: GitHub.