Node Version Manager
Living with nvm. The .nvmrc setup, the "command not found" cases, and why your native modules broke.
nvm is a shell function, not a binary. Almost every strange thing it does follows from that one fact.
Install
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
Check the repo for the current version tag. The installer appends its setup lines to your shell profile, so re-running it with a newer tag is also how you upgrade.
The commands you actually use
nvm install --lts # newest LTS
nvm install 22.11.0 # a specific version
nvm use 22 # switch this shell to the newest installed 22.x
nvm ls # what's installed, and which is active
nvm ls-remote --lts # what's available
nvm alias default 22 # what new shells start with
nvm current # same answer as `node -v`, minus the guessing
nvm uninstall 18 # reclaim the disk space
Pin the version per project
Put the version in the repo so nobody has to ask:
echo "22" > .nvmrc
Then, in the project directory:
nvm use # reads .nvmrc
nvm install # reads .nvmrc and installs it if missing
To switch automatically on cd, nvm ships a ready-made hook for zsh and bash.
The “Deeper Shell Integration” section of the README has the snippet to paste
into your profile. Do it once and an entire category of “works on my machine”
goes away.
nvm: command not found, in a place that isn’t your terminal
nvm defines a shell function, so anything that doesn’t load your interactive
profile can’t see it: cron, systemd, CI, Makefiles, GUI editors, git hooks,
sudo.
There are two fixes. Source nvm explicitly at the top of the script:
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
nvm use --silent
Or skip the shell entirely and call the version directly:
nvm exec 22 node build.js # run one command under a given version
nvm which 22 # absolute path to that node binary, for cron/PATH
Related: sudo npm install -g runs as root, which has its own PATH and its own
system node. If you find yourself typing sudo with nvm installed, something is
already wrong. nvm’s node lives in ~/.nvm and never needs it.
But I set the default version
nvm alias default applies to new shells. Terminal tabs that were already
open, and processes started before the change, keep the old version. So does a
VS Code integrated terminal spawned from a window opened earlier, so restart the
editor rather than just the panel.
If node -v disagrees with nvm current, another node is winning the PATH race:
which -a node # every node on PATH, in order
A Homebrew or .pkg install of node in /usr/local/bin or /opt/homebrew/bin
sitting ahead of nvm’s shim is the usual culprit. Uninstall it
(brew uninstall node) or make sure nvm’s line comes last in your profile.
Global packages disappeared after upgrading
Globals are installed per node version, so a new version starts empty. Carry them across at install time:
nvm install 22 --reinstall-packages-from=20
Don’t carry across npm itself (use --latest-npm instead), or anything with
native bindings. Reinstall those so they compile against the new node.
NODE_MODULE_VERSION mismatch and ERR_DLOPEN_FAILED
Native modules (sqlite3, sharp, bcrypt, canvas, most Electron deps) are
compiled against one specific node ABI. Switching node versions with an existing
node_modules gives you an error that names two module versions and explains
nothing:
npm rebuild # recompile against the current node
rm -rf node_modules && npm ci # when rebuild isn't enough
Change node version, rebuild node_modules. Make that automatic in your head.
Slow shell startup
nvm’s profile snippet runs on every new shell and adds a noticeable pause. Time it:
time zsh -i -c exit
If nvm is the cost, lazy-load it: define a stub function for node, npm and
npx that sources nvm on first use. There are one-screen snippets for this in
the nvm README’s troubleshooting section, and it typically takes shell startup
from around 500ms back to instant.
Things that catch people out
- fish shell isn’t supported. Use
nvm.fish,fnm, orasdfinstead. - nvm-windows is a different project with a different CLI (
nvm on/nvm off, no.nvmrcsupport). Commands here won’t all transfer. - On Apple Silicon, older node versions have no arm64 build, so nvm compiles from source. That’s slow, and needs Xcode command line tools. Anything from node 16 up ships a native arm64 binary.
nvm useinside a subshell doesn’t stick.(nvm use 18; node -v)affects only that subshell, and the same goes fornvm usein a script you executed rather than sourced.- Corepack, pnpm and yarn shims are per-version too. After switching node, run
corepack enableagain ifpnpmvanishes. - Each version takes 50 to 200MB and they accumulate for years. Run
nvm ls, thennvm uninstallthe ones you’ll never touch again, andnvm cache clearfor the download cache.