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
- Treat errors.Is(err, errProcessExited) exactly like 'process not found' — do not retry as if it were transient
- Close all handles you hold (os.Process.Handle / procid Token) promptly so the process object is released
- Wait for handle release with WaitForSingleObject on the process handle rather than polling Verify
- 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
- Close every os.Process/Token handle promptly after use on Windows
- Treat errProcessExited as authoritative 'process is dead', never retry as transient
- Wait on process handles (WaitForSingleObject) instead of polling Verify
- Remember third parties (AV, Task Manager) can keep PIDs resolvable after exit
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
- procid: open process %d: %w
- lock already held by another process
- procid: malformed proc stat: missing comm terminator
- procid: malformed proc stat: missing starttime
- reading another process's command line is not supported on w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/42005c5c070b6cbd.
Report an issue: GitHub.