os.homedir()
Returns the string path of the current user's home directory.
os.homedir()- Added in Node.js v2.3.0
Return value
stringThe string path of the current user's home directory.
os.homedir() gives the current user's home directory as an absolute path with no trailing separator. On POSIX it prefers the $HOME environment variable and only falls back to a passwd lookup by effective UID when $HOME is unset. On Windows it prefers %USERPROFILE% and falls back to the profile directory.
When to reach for it
Anywhere a command-line tool needs a per-user location: a config file, a credential store, a cache directory, a history file. Always compose with path.join() rather than string concatenation, so you get the right separator on every platform.
What trips people up
That environment-variable preference is the whole gotcha. Under sudo, in a systemd unit, in a CI runner or in a slim container, $HOME may be unset, empty or point somewhere unexpected — and os.homedir() will faithfully report it. os.userInfo().homedir asks the operating system directly and ignores the environment, which makes it the better choice when you specifically want the account's real home.
Node never expands ~ for you: fs.readFile('~/.config/app.json') looks for a literal directory named ~. Expand it yourself before passing the path anywhere.
Example
When to use it
- Locate a CLI tool's config file at ~/.config/<app>/config.json
- Store a downloaded credential or token in a per-user directory
- Expand a leading tilde in a user-supplied path before opening it
Frequently asked questions
Why does os.homedir() return the wrong directory under sudo?
Because it reads $HOME first and sudo may leave that pointing at the invoking user's home (or set it to root's, depending on configuration). Use os.userInfo().homedir when you want the effective account's real home from the passwd database rather than whatever the environment claims.
Does Node.js expand ~ in file paths?
No. Tilde expansion is done by the shell, so fs and path treat '~' as a literal directory name and the read fails. Replace a leading '~/' with os.homedir() using path.join() before handing the path to any filesystem call.
What is the difference between os.homedir() and os.userInfo().homedir?
os.homedir() prefers the $HOME or %USERPROFILE% environment variable and only falls back to the OS, while os.userInfo().homedir always comes straight from the operating system. They differ under sudo, in systemd units and in containers where HOME has been set explicitly.
More examples
Expand a leading tilde yourself
Node and the fs module never expand ~ — only the shell does — so a path that works when typed at a prompt fails when read from a config file. The last line is the deliberate limitation: ~alice needs a passwd lookup, so leaving it untouched is safer than guessing at a sibling directory.
const os = require('node:os');
const path = require('node:path');
function expandHome(p) {
if (p === '~') return os.homedir();
if (p.startsWith('~/') || p.startsWith('~\\')) {
return path.join(os.homedir(), p.slice(2));
}
return p; // ~otheruser is not expanded — that needs a passwd lookup.
}
console.log('plain tilde is home:', expandHome('~') === os.homedir());
console.log('joined under home:', expandHome('~/notes/todo.md').startsWith(os.homedir()));
console.log('relative untouched:', expandHome('./local.txt'));
console.log('absolute untouched:', expandHome('/etc/hosts'));
console.log('other user left alone:', expandHome('~alice/data'));Cross-platform config path with an env override
Honouring XDG_CONFIG_HOME first is what makes a tool respect a user's or a test harness's redirection instead of writing into a real home directory. Everything below it is the per-platform convention, so the same code puts files where each OS expects them.
const os = require('node:os');
const path = require('node:path');
function appDir(app) {
if (process.env.XDG_CONFIG_HOME) return path.join(process.env.XDG_CONFIG_HOME, app);
const home = os.homedir();
if (os.platform() === 'win32') {
return path.join(process.env.APPDATA ?? path.join(home, 'AppData', 'Roaming'), app);
}
if (os.platform() === 'darwin') {
return path.join(home, 'Library', 'Application Support', app);
}
return path.join(home, '.config', app);
}
console.log('config dir:', appDir('demo'));
console.log('cache dir:', path.join(appDir('demo'), 'cache'));
console.log('XDG override set:', Boolean(process.env.XDG_CONFIG_HOME));Compare the two sources of home
On a normal interactive session both agree and the branch stays quiet. They diverge under sudo, inside systemd units and in containers that set HOME explicitly, which is precisely when a tool writes its config into root's home and the user cannot find it afterwards.
const os = require('node:os');
const fromEnv = os.homedir(); // prefers $HOME / %USERPROFILE%
const fromOs = os.userInfo().homedir; // straight from the OS
console.log('os.homedir():', fromEnv);
console.log('userInfo().homedir:', fromOs);
console.log('agree:', fromEnv === fromOs);
console.log('$HOME set:', 'HOME' in process.env || 'USERPROFILE' in process.env);
if (fromEnv !== fromOs) {
console.log('environment overrides the account home — sudo, a unit file or a container');
}
Discussion