gastownhall/beads · info

procid: process has exited

Error message

procid: process has exited

What it means

errProcessExited is a Windows sentinel: GetExitCodeProcess returned the exited state, but the PID is still resolvable because some handle (ours, Task Manager, antivirus) keeps the process object alive. The library returns it from Verify, Signal, tokenForProcess, and IsProcessGone so that 'Verify == true implies running' holds on Windows — callers should treat it identically to a nonexistent PID.

Source

Thrown at internal/procid/procid_windows.go:24

	"errors"
	"fmt"
	"os"
	"strconv"

	"golang.org/x/sys/windows"
)

// Handle keeps a Windows process handle open to prevent PID reuse.
type Handle struct {
	process windows.Handle
	token   Token
}

// errProcessExited marks a process which is terminated but whose PID is still
// resolvable because some handle (ours or a third party's, such as Task
// Manager or an antivirus scanner) keeps the process object alive. Treating
// it as gone keeps the invariant "Verify == true implies running" on Windows.
var errProcessExited = errors.New("procid: process has exited")

// stillActive is the GetExitCodeProcess sentinel for a running process
// (STILL_ACTIVE, 259).
const stillActive = 259

func Capture(pid int) (Token, error) {
	process, err := openProcess(pid, windows.PROCESS_QUERY_LIMITED_INFORMATION)
	if err != nil {
		return "", fmt.Errorf("procid: open process %d: %w", pid, err)
	}
	defer func() { _ = windows.CloseHandle(process) }()
	return tokenForProcess(process)
}

func Verify(pid int, tok Token) (bool, error) {
	process, err := openProcess(pid, windows.PROCESS_QUERY_LIMITED_INFORMATION)
	if err != nil {
		if errors.Is(err, windows.ERROR_INVALID_PARAMETER) {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Treat errors.Is(err, errProcessExited) exactly like 'process not found' — do not retry as if it were transient
  2. Close all handles you hold (os.Process.Handle / procid Token) promptly so the process object is released
  3. Wait for handle release with WaitForSingleObject on the process handle rather than polling Verify
  4. If your tooling (AV, Task Manager) holds handles, this is expected — treat the PID as dead

Example fix

// before
tok, err := procid.Capture(pid)
if err != nil { return err } // errProcessExited treated as fatal
// after
tok, err := procid.Capture(pid)
if err != nil {
    if errors.Is(err, procid.ErrProcessExited) { return ErrProcessGone } // sentinel: treat as exited
    return err
}
Defensive patterns

Strategy: type-guard

Type guard

func isProcessExited(err error) bool { return errors.Is(err, errProcessExited) } // via exported sentinel/errors.Is

Try / catch

if err != nil {
    if errors.Is(err, procid.ErrProcessExited) { return ErrProcessGone } // treat as dead, not transient
    return err
}

Prevention

When it happens

Trigger: Calling Verify/Signal/tokenForProcess/IsProcessGone on a PID whose process has terminated but whose process object is still referenced by any open handle (own Handle leak, Task Manager, AV scanner) — GetExitCodeProcess returns STILL_ACTIVE-contrast exit state and the library surfaces errProcessExited.

Common situations: Forgetting to Close() an os.Process or handle, keeping the object alive after child exit; antivirus or monitoring tools holding handles on Windows; PID-reuse checks racing with slow handle release.

Related errors


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