Schniz/fnm · error
Can't read user input
Error message
Can't read user input
What it means
When `fnm use <version>` cannot find the requested installed version, it prints a colored install prompt and reads one line from stdin. If `read_line` returns an io error — stdin closed (EBADF), redirected to something unreadable, or an I/O failure — `.expect("Can't read user input")` panics. Plain EOF (e.g. stdin from /dev/null) is not an error: it leaves the answer empty, which counts as 'no'.
Source
Thrown at src/commands/use.rs:194
fn should_install_interactively(requested_version: &UserVersion) -> bool {
use std::io::{IsTerminal, Write};
if !(std::io::stdout().is_terminal() && std::io::stdin().is_terminal()) {
return false;
}
let error_message = format!(
"fnm can't find an installed Node version matching {}.",
requested_version.to_string().italic()
);
eprintln!("{}", error_message.red());
let do_you_want = format!("Do you want to install it? {} [y/N]:", "answer".bold());
eprint!("{} ", do_you_want.yellow());
std::io::stdout().flush().unwrap();
let mut s = String::new();
std::io::stdin()
.read_line(&mut s)
.expect("Can't read user input");
s.trim().to_lowercase() == "y"
}
fn warn_if_multishell_path_not_in_path_env_var(
multishell_path: &std::path::Path,
config: &FnmConfig,
) {
if let Some(path_var) = std::env::var_os("PATH") {
let bin_path = if cfg!(unix) {
multishell_path.join("bin")
} else {
multishell_path.to_path_buf()
};
let fixed_path = bin_path.to_str().and_then(shell::maybe_fix_windows_path);
let fixed_path = fixed_path.as_deref();
View on GitHub (pinned to 86adc9676c)
Solutions
- Pre-install so the prompt never appears: `fnm install 18 && fnm use 18`.
- Give stdin a safe fallback that declines gracefully: `fnm use 18 </dev/null` (EOF → answer 'no').
- In non-interactive contexts, prefer `fnm use --version-file` flows or set FNM_VERSION_FILE_STRATEGY and ensure versions are pre-installed.
- If patching fnm: treat a read error as 'no' instead of panicking — `.unwrap_or_default()` on the read result.
Example fix
// before (src/commands/use.rs)
std::io::stdin().read_line(&mut s).expect("Can't read user input");
// after
let _ = std::io::stdin().read_line(&mut s); // read failure => empty answer => "no" Defensive patterns
Strategy: validation
Validate before calling
# only attempt interactive fnm use when stdin is readable if [ -t 0 ] || [ -r /dev/stdin ]; then fnm use "$VERSION" </dev/tty 2>/dev/null || fnm use "$VERSION" else fnm install "$VERSION" && fnm use "$VERSION" # non-interactive path, no prompt fi
Try / catch
let mut s = String::new();
match std::io::stdin().read_line(&mut s) {
Ok(_) => s.trim().eq_ignore_ascii_case("y"),
Err(_) => false, // unreadable stdin => treat as "no", never panic
} Prevention
- Pre-install required versions in CI/cron so the y/N prompt never triggers.
- Redirect stdin from /dev/null in non-interactive scripts (`fnm use v18 </dev/null` declines cleanly).
- Attach a TTY (`</dev/tty`) when you genuinely want the prompt inside wrappers.
When it happens
Trigger: Running `fnm use <missing-version>` in a context where stdin is closed or not open for reading — `fnm use 18 0<&-`, stdin redirected to a directory, or fnm invoked from a daemon/scheduled task/CI job with no console attached.
Common situations: CI pipelines and cron jobs calling `fnm use` without pre-installing the version; scripts run with detached stdin; IDE external tasks with no terminal; `fnm use` inside hooks (pre-commit, direnv) where stdin is consumed or closed.
Related errors
- Can't write output
- Can't get a temporary file
- Failed to grab exit code
- Invalid OS string
- choosing base strategy
AI-assisted analysis of Schniz/fnm@86adc9676c (2026-08-16).
Data as JSON: /api/errors/79badc702802ea42.
Report an issue: GitHub.