Schniz/fnm · critical

Failed to grab exit code

Error message

Failed to grab exit code

What it means

`fnm exec --using <ver> <cmd...>` spawns the resolved binary with inherited stdio and calls `.wait()` on the child to obtain its ExitStatus. If the OS fails to reap the child (an io::Error such as ECHILD — the pid was already reaped elsewhere), `.expect("Failed to grab exit code")` panics instead of returning. This is distinct from `Error::CantReadProcessExitCode`, which is the graceful error returned when the child was killed by a Unix signal (`exit_status.code()` is None).

Source

Thrown at src/commands/exec.rs:108

        log::debug!(
            "Running {binary} with PATH={path_env}",
            path_env = path_env.display()
        );

        let exit_status = Command::new(binary)
            .args(arguments)
            .stdin(Stdio::inherit())
            .stdout(Stdio::inherit())
            .stderr(Stdio::inherit())
            .env("PATH", path_env)
            .spawn()
            .map_err(|source| Error::CantSpawnProgram {
                source,
                binary: binary.clone(),
            })?
            .wait()
            .expect("Failed to grab exit code");

        let code = exit_status.code().ok_or(Error::CantReadProcessExitCode)?;
        std::process::exit(code);
    }
}

#[derive(Debug, Error)]
pub enum Error {
    #[error("Can't spawn program: {source}\nMaybe the program {} does not exist on not available in PATH?", binary.bold())]
    CantSpawnProgram {
        source: std::io::Error,
        binary: String,
    },
    #[error("Can't read path environment variable")]
    CantReadPathVariable,
    #[error("Can't add path to environment variable: {}", source)]
    CantAddPathToEnvironment { source: std::env::JoinPathsError },
    #[error("Can't find version in dotfiles. Please provide a version manually to the command.")]

View on GitHub (pinned to 86adc9676c)

Solutions

  1. Retry the command once — a reaping race is often transient.
  2. Don't wrap detached/daemonizing programs with `fnm exec`; apply the env first (`eval "$(fnm env)"; mytool`) and run the tool directly.
  3. Check kernel logs (`dmesg`) for OOM kills if children vanish.
  4. If patching fnm: replace `.expect(...)` with `.map_err(...)?` and propagate a graceful error mirroring CantSpawnProgram.

Example fix

// before (src/commands/exec.rs)
.wait()
.expect("Failed to grab exit code");

// after
let exit_status = child
    .wait()
    .map_err(|source| Error::CantWaitForProcess { source })?;
Defensive patterns

Strategy: try-catch

Try / catch

let exit_status = Command::new(binary).args(args).spawn()
    .map_err(|source| Error::CantSpawnProgram { source, binary })?
    .wait();
match exit_status {
    Ok(status) => {
        let code = status.code()
            .ok_or(Error::CantReadProcessExitCode)?; // signal-killed child
        std::process::exit(code);
    }
    Err(e) if e.kind() == std::io::ErrorKind::Interrupted => /* retry wait once */,
    Err(e) => return Err(anyhow::anyhow!("failed to reap child process: {e}")),
}

Prevention

When it happens

Trigger: `fnm exec --using <ver> <cmd>` where wait() itself errors: the child daemonizes/forks and its pid gets reaped by another party, a supervisor/tracer (strace, valgrind, procdump) interferes with child reaping, or the process is killed externally in a way that races the wait call.

Common situations: Wrapping self-daemonizing tools (daemons, `npm run` scripts that spawn and detach, Windows GUI tools) with `fnm exec`; running under sandboxing or observability layers that reap children; PID-namespace/container edge cases; OOM-killer racing the wait.

Related errors


AI-assisted analysis of Schniz/fnm@86adc9676c (2026-08-16). Data as JSON: /api/errors/2fd52f61edb6220d. Report an issue: GitHub.