golang/go · warning

%w: output pipes not closed after waiting %v

Error message

%w: output pipes not closed after waiting %v

What it means

Wraps exec.ErrWaitDelay when cmd.Wait times out waiting for output pipes to close after the process has exited. The original Wait error is preserved (%w) and augmented with the configured WaitDelay duration. This indicates the child process exited but left stdout or stderr pipe handles open (typically via a lingering grandchild process inheriting the FDs).

Source

Thrown at src/cmd/internal/script/cmds.go:485

		if err == nil {
			break
		}
		if isETXTBSY(err) {
			// If the script (or its host process) just wrote the executable we're
			// trying to run, a fork+exec in another thread may be holding open the FD
			// that we used to write the executable (see https://go.dev/issue/22315).
			// Since the descriptor should have CLOEXEC set, the problem should
			// resolve as soon as the forked child reaches its exec call.
			// Keep retrying until that happens.
		} else {
			return nil, err
		}
	}

	wait := func(s *State) (stdout, stderr string, err error) {
		err = cmd.Wait()
		if errors.Is(err, exec.ErrWaitDelay) {
			err = fmt.Errorf("%w: output pipes not closed after waiting %v", err, cmd.WaitDelay)
		}
		return stdoutBuf.String(), stderrBuf.String(), err
	}
	return wait, nil
}

// lookPath is (roughly) like exec.LookPath, but it uses the script's current
// PATH to find the executable.
func lookPath(s *State, command string) (string, error) {
	var strEqual func(string, string) bool
	if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
		// Using GOOS as a proxy for case-insensitive file system.
		// TODO(bcmills): Remove this assumption.
		strEqual = strings.EqualFold
	} else {
		strEqual = func(a, b string) bool { return a == b }
	}

View on GitHub (pinned to b6b368adc5)

Solutions

  1. Increase cmd.WaitDelay to give descendant processes more time to close pipes.
  2. Ensure the exec'd command closes or redirects inherited stdout/stderr (e.g. redirect to /dev/null or a file).
  3. Use process groups (Setpgid) and kill the entire group to clean up lingering descendants.
  4. If using a shell wrapper, add `exec` before the final command or redirect fds: `cmd > /dev/null 2>&1`.

Example fix

// Before: pipes inherited by descendants
// cmd := exec.Command("sh", "-c", "server & wait")

// After: redirect so descendants don't hold pipes
// cmd := exec.Command("sh", "-c", "server >/dev/null 2>&1 & wait")
// cmd.WaitDelay = 5 * time.Second
Defensive patterns

Strategy: validation

Validate before calling

// Set WaitDelay and ensure child processes don't inherit pipes:
cmd := exec.Command("sh", "-c", script)
cmd.Stdout = &stdoutBuf
cmd.Stderr = &stderrBuf
cmd.WaitDelay = 10 * time.Second
// In the script, redirect inherited fds:
//   mydaemon >/dev/null 2>&1 &

Try / catch

// Detect and handle ErrWaitDelay gracefully:
err = cmd.Wait()
if errors.Is(err, exec.ErrWaitDelay) {
    // Process exited but pipes weren't closed in time.
    // Output is still available in stdoutBuf/stderrBuf.
    log.Printf("warning: process exited but pipes delayed: %v", err)
    err = nil // or treat as non-fatal
}

Prevention

When it happens

Trigger: cmd.Wait() returns exec.ErrWaitDelay, meaning the WaitDelay timer expired before the output pipes were fully closed. The child process exited but descendant processes (or the OS) are still holding the pipe file descriptors open.

Common situations: The exec'd command spawns child processes that inherit stdout/stderr and outlive the parent. Common with shell pipelines, daemon-spawning commands, or programs that fork without closing inherited FDs. The WaitDelay (configurable on exec.Cmd) provides a grace period; if it expires, this error fires.

Related errors


AI-assisted analysis of golang/go@b6b368adc5 (2026-08-12). Data as JSON: /api/errors/c6bfa22f53117824. Report an issue: GitHub.