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 int

View on GitHub (pinned to 71377f2769)

Solutions

  1. Treat errors.Is(err, unix.ESRCH) as 'process is gone' and take the dead-process branch (do not retry)
  2. If a zombie is unexpected, have the parent call wait()/waitpid() to reap it, then re-check
  3. 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

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


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