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
- Retry the command once — a reaping race is often transient.
- Don't wrap detached/daemonizing programs with `fnm exec`; apply the env first (`eval "$(fnm env)"; mytool`) and run the tool directly.
- Check kernel logs (`dmesg`) for OOM kills if children vanish.
- 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
- Don't wrap self-daemonizing programs with `fnm exec`; export the env and run them directly.
- In wrapper scripts, treat a non-zero/absent fnm exit as 'run without fnm' rather than crashing.
- Keep tracing/supervision layers from reaping fnm's children.
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
- Can't write output
- Can't get a temporary file
- Can't read user input
- Invalid OS string
- choosing base strategy
AI-assisted analysis of Schniz/fnm@86adc9676c (2026-08-16).
Data as JSON: /api/errors/2fd52f61edb6220d.
Report an issue: GitHub.