gastownhall/beads · warning
procid: process is no longer running: %w
Error message
procid: process is no longer running: %w
What it means
parseStartTime parses field 22 (starttime) from /proc/<pid>/stat to build a process-birth identity token. When the state field (field 3) is 'Z' (zombie), 'X', or 'x' (dead), the process has exited, so the function returns this error wrapping unix.ESRCH — the conventional 'no such process' signal that the target is gone rather than malformed.
Source
Thrown at internal/procid/procid_linux.go:212
if err != nil {
return "", &processStatReadError{pid: pid, err: err}
}
return parseStartTime(string(data))
}
func parseStartTime(stat string) (string, error) {
endComm := strings.LastIndex(stat, ")")
if endComm == -1 {
return "", errors.New("procid: malformed proc stat: missing comm terminator")
}
fields := strings.Fields(stat[endComm+1:])
// The remainder starts with state (field 3), so starttime (field 22) is
// its twentieth field.
if len(fields) < 20 {
return "", errors.New("procid: malformed proc stat: missing starttime")
}
if fields[0] == "Z" || fields[0] == "X" || fields[0] == "x" {
return "", fmt.Errorf("procid: process is no longer running: %w", unix.ESRCH)
}
if _, err := strconv.ParseUint(fields[19], 10, 64); err != nil {
return "", fmt.Errorf("procid: malformed proc stat starttime: %w", err)
}
return fields[19], nil
}
type bootIDReadError struct {
path string
err error
}
func (e *bootIDReadError) Error() string {
return fmt.Sprintf("procid: read boot ID %s: %v", e.path, e.err)
}
type processStatReadError struct {
pid intView on GitHub (pinned to 71377f2769)
Solutions
- Treat errors.Is(err, unix.ESRCH) as 'process is gone' and take the dead-process branch (do not retry)
- If a zombie is unexpected, have the parent call wait()/waitpid() to reap it, then re-check
- Re-run Capture after confirming the PID is live (e.g. after reaping) if you need a fresh token
Example fix
// before
tok, err := procid.Capture(pid)
if err != nil {
return err
}
// after
tok, err := procid.Capture(pid)
if errors.Is(err, unix.ESRCH) {
return nil // process already exited; nothing to do
}
if err != nil {
return err
} Defensive patterns
Strategy: type-guard
Validate before calling
func processExists(pid int) bool {
_, err := os.Stat(fmt.Sprintf("/proc/%d/stat", pid))
return err == nil
} Type guard
func isProcessGone(err error) bool {
return errors.Is(err, unix.ESRCH) || errors.Is(err, os.ErrNotExist)
} Try / catch
tok, err := procid.Capture(pid)
if isProcessGone(err) {
return handleExitedProcess(pid) // reap via wait() if you are the parent
}
if err != nil {
return fmt.Errorf("capture pid %d: %w", pid, err)
} Prevention
- If you are the parent, wait()/waitpid() promptly to avoid zombie children
- Re-check liveness (kill(pid,0) or /proc stat) immediately before Capture
- Treat ESRCH as a normal exit condition, not a failure to log at error level
- Design flows so token capture happens immediately after spawning to minimize exit races
When it happens
Trigger: Calling Capture or processStartTime on a PID that has already exited and been reaped or is awaiting reap (zombie); a race where the process dies between opening /proc/<pid>/stat and reading it; verifying a token against a stale PID whose owner is now a zombie.
Common situations: Parent process has not wait()ed yet so child lingers as zombie; worker pool reaping races; health checks probing a PID that just exited; kill-then-verify flows where verify lands after exit.
Related errors
- procid: malformed proc stat: missing comm terminator
- procid: malformed proc stat: missing starttime
- procid: process %d still matches token after fatal signal an
- procid: process has exited
- procid: pidfd open %d: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/895515b31c5cafa5.
Report an issue: GitHub.