go-delve/delve · critical

wait err %s %d

Error message

wait err %s %d

What it means

The wait() system call invoked by trapWaitInternal returned an error while Delve was waiting for a tracee to stop, exit, or signal. trapWait is the core event loop of the native Linux backend: every breakpoint hit, thread exit, and manual stop flows through it, so any wait failure (EINTR races aside) indicates the debugger lost sync with its tracees.

Source

Thrown at pkg/proc/native/proc_linux.go:456

)

func trapWaitInternal(procgrp *processGroup, pid int, options trapWaitOptions) (*nativeThread, error) {
	var waitdbp *nativeProcess = nil
	if len(procgrp.procs) == 1 {
		// Note that waitdbp is only used to call (*nativeProcess).wait which will
		// behave correctly if waitdbp == nil.
		waitdbp = procgrp.procs[0]
	}

	halt := options&trapWaitHalt != 0
	for {
		wopt := 0
		if options&trapWaitNohang != 0 {
			wopt = sys.WNOHANG
		}
		wpid, status, err := waitdbp.wait(pid, wopt)
		if err != nil {
			return nil, fmt.Errorf("wait err %s %d", err, pid)
		}
		if wpid == 0 {
			if options&trapWaitNohang != 0 {
				return nil, nil
			}
			continue
		}
		dbp := procgrp.procForThread(wpid)
		var th *nativeThread
		if dbp != nil {
			var ok bool
			th, ok = dbp.threads[wpid]
			if ok {
				th.Status = (*waitStatus)(status)
			}
		} else {
			dbp = procgrp.procs[0]
		}

View on GitHub (pinned to a23773e6c3)

Solutions

  1. Check whether the debuggee already exited (dlv will usually report ErrProcessExited on the next command); reconnect or restart the debug session.
  2. Retry the wait: transient EINTR errors usually resolve on the next call.
  3. Ensure only one tracer is attached — other tools (strace, another dlv) reaping tracees cause ECHILD.
  4. If it reproduces in containers, run with --init (proper child reaping, e.g. tini) and PID 1 semantics.
  5. Upgrade Delve: several wait-loop robustness fixes (EINTR handling) have landed over time.
Defensive patterns

Strategy: try-catch

Validate before calling

// before waiting, confirm the process is still traceable and not reaped
func stillTraced(pid int) error {
    b, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid))
    if err != nil {
        return fmt.Errorf("process %d gone: %w", pid, err)
    }
    if !strings.Contains(string(b), "State:\tT (stopped)") && !strings.Contains(string(b), "t (tracing stop)") {
        // state may have changed; not fatal, but log it
        return nil
    }
    return nil
}

Type guard

func isWaitErr(err error) bool {
    return err != nil && strings.Contains(err.Error(), "wait err ")
}

Try / catch

th, err := trapWait(procgrp, pid)
if err != nil {
    var exited proc.ErrProcessExited
    if errors.As(err, &exited) {
        // debuggee died: clean up and end session gracefully
        detachAndCleanup()
        return fmt.Errorf("debuggee exited with status %d", exited.Status)
    }
    if isWaitErr(err) && errors.Is(syscall.ECHILD, err) {
        return fmt.Errorf("tracee reaped by another process; check for concurrent strace/dlv")
    }
    return err
}

Prevention

When it happens

Trigger: trapWait / trapWaitNohang (via stop or exitGuard): waitdbp.wait(pid, wopt) returns an error — typically ECHILD (all children reaped / process already exited and was collected elsewhere), EINTR propagated without retry, or EINVAL from an options/pid mismatch in __waitpid.

Common situations: The debugged process exited concurrently (dlv attach to a process that dies mid-session) yielding ECHILD; signal storms causing repeated EINTR; running under environment wrappers (IDE launches, test harnesses) that interfere with child reaping; multi-process follow-exec sessions where a child was reaped by another thread of the debugger.

Related errors


AI-assisted analysis of go-delve/delve@a23773e6c3 (2026-08-31). Data as JSON: /api/errors/3e0db8dffdc5cdd5. Report an issue: GitHub.