Practical guide on how to write portable/cross-platform Node.js code.
Why you should care
About this guide
If you find this document too long, you can jump to the summary.
If you want to keep up to date on portability issues introduced with each new Node.js release, follow @ehmicky on Twitter.
Did you find an error or want to add more information? Issues and PRs are welcome!
Table of contents
- Installing and updating Node
- Core utilities
- C/C++ addons
- Directory locations
- System configuration
- Character encoding
- EOF and BOM
- File paths
- Files execution
- Package binaries
- Environment variables
- File metadata
- Time resolution
- OS identification
- Device information
- Further reading
Installing and updating Node
Installers for each major OS are available on the Node.js website.
npm on Windows, it is convenient to use
Each OS has its own set of (from the lowest to the highest level):
Directly executing one of those binaries (e.g. calling
sed) won't usually
work on every OS.
There are several approaches to solve this:
- Most Node.js API core modules abstract this (mostly through
libuv). E.g. the
child_processmethods are executing OS-specific system calls under the hood.
- some projects abstract OS-specific core utilities like:
- some projects abstract common user applications:
Few lower-level tools attempt to bring cross-platform compatibility by emulating or translating system calls:
- Wine: to run Windows API calls on Linux, Mac, BSD and Solaris.
- Cygwin: to run POSIX on Windows.
- WSL: to run the Linux command line on Windows (ELF binary execution, system calls, filesystem, Bash, core utilities, common applications).
It is recommended to run automated tests on a continuous integration provider that supports Linux, Mac and Windows, which most high-profile providers now do.
Typical directory locations are OS-specific:
- the main temporary directory could for example be
/var/folders/.../Ton Mac or
os.tmpdir()can be used to retrieve it on any OS.
- the user's home directory could for example be
/Users/USERon Mac or
os.homedir()can be used to retrieve it on any OS.
While Unix usually stores system configuration as files, Windows uses the registry, a central key-value database. Some projects like node-winreg, rage-edit or windows-registry-node can be used to access it from Node.
This should only be done when accessing OS-specific settings. Otherwise storing configuration as files or remotely is easier and more portable.
The character encoding on Unix is usually UTF-8. However on Windows it is usually either UTF-16 or one of the Windows code pages. Few non-Unicode character encodings are also popular in some countries. This can result in characters not being printed properly, especially high Unicode code points and emoji.
The character encoding can be specified using an
encoding option with most
relevant Node.js core methods.
UTF-8 is always the default value except
crypto methods where
the default instead.
UTF-16 little endian,
ASCII, except for
UTF-16 little endian and
UTF-16 big endian by default. If
Node.js is built with
full internationalization support
or provided with it at runtime,
many more character encodings
are supported by
If doing so is inconvenient,
iconv can be used instead.
It is recommended to always use UTF-8. When reading from a file or terminal, one should either:
- detect the character encoding using
jschardetand convert to UTF-8.
- validate and/or document that the input should be in UTF-8.
When writing to a terminal the character encoding will almost always be
UTF-8 on Unix and
CP866 on Windows (
log-symbols can be used to
print common symbols consistently across platforms.
The character representation of a
newline is OS-specific. On Unix it
\n (line feed) while on Windows it is
\r\n (carriage return followed by
Newlines inside a template string translate to
\n on any OS.
const string = `this is an example`
Some Windows applications, including the
cmd.exe terminal, print
newlines, so using
\n will work just fine. However some Windows applications
don't, which is why when reading from or writing to a file the OS-specific
os.EOL should be used
EOF and BOM
The substitute character
stops file streams
on some Windows commands when in text mode. This includes the
type command in
cmd.exe. As a consequence
that character should be avoided in non-binary files.
As opposed to Windows, Unix does not implicitely add a newline at the end of files. Thus it is recommended to end files with a newline character. However please remember that Windows will print these as if two newlines were present instead.
The BOM is a special character at the beginning of a file indicating its endianness and character encoding. Since it creates issues with shebangs and adds little value to UTF-8, it is better not to add it to new files. However if a BOM is present in input, it should be properly handled. Fortunately this is the default behavior of Node.js core methods, so this should usually not create any issues.
/ is used as a file path delimiter on Unix (
used on Windows instead (
\file\to\path). The path delimiter can be retrieved
actually allows using or mixing in
/ delimiters in file paths most of the
time, but not always so this should not be relied on.
Furthermore absolute paths always start with
/ on Unix, but on Windows they
\: the current drive.
C:\: a specific drive (here
C:). This can also be used with relative paths like
\\HOST\: UNC path, for remote hosts.
\\?\: allows to overcome file path length limit of 260 characters. Those can be produced in Node.js with
\\.\: device path.
When file paths are used as arguments to Node.js core methods:
- for example as arguments to
- only Unix paths are allowed on Unix. Both Unix and Windows paths are allowed on Windows (including mixed).
When file paths are returned by Node.js core methods:
- for example the return values of
os.tmpdir()or the value of
- Unix paths are returned on Unix and Windows paths on Windows.
Outside of Node.js, i.e. when the path is input from (or output to) the terminal or a file, its syntax is OS-specific.
- if a path must be output outside of Node.js (e.g. terminal or file),
path.normalize()should be used to make it OS-specific.
- if a path comes from outside of Node.js or from a core method, it will be OS-specific. However all Node.js core methods will properly handle it.
- in all other cases using Unix paths will just work.
Each OS tends to use its own file system: Windows uses NTFS, Mac uses APFS (previously HFS+) and Linux tends to use ext4, Btrfs or XFS. Each file system has its own restrictions when it comes to naming files and paths.
Portable filenames need to avoid:
- any other characters but
- starting with
- ending with a
- uppercase characters (Mac and Windows are case-insensitive).
- being more than 255 characters long.
- being one of
Portable file paths need to avoid:
more than 260
This used to
node_modulesbut not anymore with the latest
- use the
~userhome directory shorthand.
Writing interoperable shell code can be somewhat achieved by using either:
However this won't work on Windows which uses two other shells:
cmd.exe is very different from Bash and has quite many limitations:
;cannot be used to separate statements. However
&&can be used like in Bash.
- CLI flags often use slashes (
/opt) instead of dashes (
-opt). But Node.js binaries can still use
- Globbing (e.g. wildcard
*) does not work.
- Exit code are accessed with
- Escaping is done differently with
double quotes and
^. This is partially solved with the
windowsVerbatimArgumentswhich defaults to
When the option
/bin/sh will be used on Unix and
cmd.exe (or the environment
ComSpec) will be used on Windows. Since those shells behave
differently it is better to avoid that option.
As a consequence it is recommended to:
- keep shell commands to simple
execa.shell()) to fire those.
do not work on Windows, where only files ending with
.bat can be directly executed. Portable file execution must either:
- use an interpreter, e.g.
node file.jsinstead of
cross-spawn(which is included in
During file execution the extension can be omitted on Windows if it is listed
variable, which defaults to
.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC. This won't work on
Finally the option
does not modify
process.title on Windows.
Many of those differences can be solved by using
are installed in the
node_modules/.bin directory by
On Unix those are symlinks pointing to the executable files. They can be executed directly inside a terminal.
On Windows, each package binary creates instead two files for the same purpose:
- a Windows batch file ending with
.cmdwhich can be executed directly inside
- a Unix shell file with no file extension which can be executed with
The syntax to
reference environment variables is
$VARIABLE on Unix but
%VARIABLE% on Windows. Also if the variable is
missing, its value will be
'' on Unix but
'%VARIABLE%' on Windows.
to a command, it must be prepended with
VARIABLE=value ... on Unix. However on
Windows one must use
Set VARIABLE=value or
setx VARIABLE value as separate
cross-env can be used
to both reference and pass environment variables on any OS.
Environment variables are case insensitive on Windows but not on Unix.
path-key can be used to solve this
PATH environment variable.
Finally most environment variables names are OS-specific:
SHELLon Unix is
ComSpecon Windows. Unfortunately
PS1on Unix is
PWDon Unix is
process.chdir()should be used instead.
HOMEon Unix is
os.userInfo().homedir(more accurate) should be used instead.
TMPDIRin Unix is
os.tmpdir()should be used instead.
LOGNAMEon Unix is
os.userInfo().usernameshould be used instead.
HOSTNAMEon Unix is
os.hostname()should be used instead.
osenv can be used to retrieve
OS-specific environment variables names.
Creating regular symlinks on Windows will most likely fail because it requires a "create symlink" permission which by default is off for non-admins. Also some file systems like FAT do not allow symlinks. As a consequence it is more portable to copy files instead of symlinking them.
Neither junctions nor hard links
require permissions on Windows.
Unix uses POSIX permissions but Windows is based on a combination of:
- file attributes
hidefilecan be used to manipulate those.
- ACLs (also called NTFS permissions or just "file permissions").
- share permissions.
readonlyfile attribute on Windows. This is quite limited as it does not check other file attributes nor ACLs.
readonlyfile attribute is checked on Windows when the
writePOSIX permission is missing for any user class (
Another difference on Windows: to execute files their extension must be listed
in the environment variable
is only available on Mac.
initgroups()throw an error.
fs.chown()does not do anything.
The resolution of
is hardware-specific and varies between 1 nanosecond and 1 millisecond.
os core module offers some
finer-grained identification methods but those are rarely needed:
os.type()is similar but slighly more precise.
os.release()returns the OS version number, e.g.
os.arch()(or the identical
process.arch) returns the CPU architecture, e.g.
os.endianness()returns the CPU endianness, i.e.
Some projects allow retrieving:
getos: the Linux distribution name.
osname(and the related
macos-release): the OS name and version in a human-friendly way.
is-windows: whether current OS is Windows, including through MSYS and Cygwin.
is-wsl: whether current OS is Windows though WSL.
be used for more device information.
However on Windows:
- sockets / named pipes must be prefixed with
- TCP servers cannot
listen()on a file descriptor.
SCHED_RRis inefficient, so the default value is
Other projects can be used to manipulate processes:
ps-list: list processes.
fastlistcan also be used for Windows only.
pid-from-port: find processes by port.
process-exists: check if a process is running.
Windows do not use signals like Unix does.
Which signals can be used is OS-specific:
process.on(signal)can only use the following signals on Windows:
process.kill()) can be used on Windows with:
SIGUNUSEDcan only be used on Linux.
SIGINFOcan only be used on Mac.
Each signal has both an OS-agnostic name and an OS-specific integer constant.
can use either. It is possible to convert between both using
However it is more portable to use signal names instead of integer constants.
does not work on Windows.
Node errors can be identified with either:
- instead of
- do not rely on OS system calls or core utilities without using an abstraction layer.
- test each OS with virtual machines and continuous integration.
npm install -g windows-build-toolson Windows when installing C/C++ addons.
osNode.js core module when needed.
UTF-8. File/terminal input should either be validated or converted to it (
- avoid printing Unicode characters (including emoji) except through projects like figures and log-symbols.
os.EOLwhen reading from or writing to a file,
- end files with a newline.
- use editorconfig.
- avoid the
CTRL-Z) in non-binary files.
path.normalize()when writing a file path to a terminal or file. Otherwise use Unix paths (slashes).
- only use lowercase
- avoid paths longer than 260 characters.
- fire shell commands with
- keep shell commands to simple
command arguments...calls, optionally chained with
- reference and pass environment variables to shell commands using
- copy files instead of symlinking them.
chokidarto watch files.
- do not assume
- when using OS-specific logic identify the current OS with
systeminformationto retrieve any device information not available through the
- sockets / named pipes must be prefixed with
- TCP servers should not
listen()on a file descriptor.
process-existsto find and check for processes.
fkillto terminate processes.
- only use
process.kill()with the following signals:
- only use
process.on(signal)with the following signals:
- do not use
We also invite you to submit links to related projects but only if:
- it's specific to Node.js
- it provides cross-platform support that is not available in core Node.js
- it's maintained
Thanks goes to these wonderful people:
Michael J. Ryan