path.join()

Joins path segments with the platform separator and normalizes the result.

Signature
path.join([...paths])
  • Added in Node.js v0.1.16

Parameters

ParameterTypeDescription
$...pathsoptionalstringA sequence of path segments to join. Zero-length segments are ignored.

Return value

stringThe joined and normalized path, or '.' if the joined result is zero-length.

Errors

Throws a TypeError (ERR_INVALID_ARG_TYPE) if any segment is not a string.

path.join() glues segments together with the platform separator and then runs the result through path.normalize(). That normalization pass is the whole point: '..' segments are collapsed against the segment before them, '.' segments disappear, and runs of repeated separators become one.

When to reach for it

Use it whenever you are building a path out of pieces you already have — a base directory plus a filename, a route parameter appended to an uploads folder, a config value joined to a project root. It is the default choice for relative paths, because unlike path.resolve() it never consults the current working directory and never makes the result absolute.

What trips people up

An absolute segment in the middle does not restart the path. path.join('/a/b', '/c/d') is '/a/b/c/d', while path.resolve('/a/b', '/c/d') is '/c/d'. If you want the later argument to win, you want resolve.

It also does not sanitize input. Joining a root with user-supplied '../../etc/passwd' happily escapes the root, because collapsing '..' is exactly what it is supposed to do. Containment must be checked afterwards with path.relative().

Trailing separators survive normalization, so path.join('a', 'b/') keeps its trailing slash.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Building a path to a file inside a known directory without hardcoding '/' or '\\'
  • Assembling an output filename from a configured output directory and a generated basename
  • Constructing nested asset paths in a build script that must work on Windows and Linux

Frequently asked questions

What is the difference between path.join and path.resolve?

path.join concatenates segments and normalizes, while path.resolve walks the arguments right to left until it has an absolute path, falling back to the current working directory. join('/a/b', '/c/d') gives '/a/b/c/d'; resolve('/a/b', '/c/d') gives '/c/d'. Use join for relative composition, resolve when you need an absolute path.

Why does path.join return a single dot?

Because the joined result was zero-length. path.join('a', '..') cancels out completely, and rather than return an empty string — which no filesystem call accepts — Node returns '.', meaning the current working directory. Passing no arguments at all returns '.' for the same reason.

Is path.join safe against path traversal?

No. path.join collapses '..' segments as designed, so a base directory joined with '../../etc/passwd' resolves outside that base. To confine a path, resolve both the root and the target, then check that path.relative(root, target) does not start with '..' and is not absolute.

More examples

join() versus resolve() on a mid-path absolute segment

join() treats every argument as a segment, so the leading slash on '/c/d' is just another separator to normalize. resolve() walks right to left and stops at the first absolute segment, discarding everything to its left. This is the single most common source of surprise when swapping one for the other.

Example · javascript
const path = require('node:path');

console.log(path.join('/a/b', '/c/d'));
// -> /a/b/c/d

console.log(path.posix.resolve('/a/b', '/c/d'));
// -> /c/d

Building and reading a real file path

The same joined string is used to create the directory, write the file and read it back, which is the normal shape of join() in real code. The path stays relative to the current directory throughout, so nothing depends on where the script was launched from.

Example · javascript
const path = require('node:path');
const fs = require('node:fs');

const dir = path.join('reports-demo', 'q1');
fs.mkdirSync(dir, { recursive: true });

const file = path.join(dir, 'sales.csv');
fs.writeFileSync(file, 'region,total\nnorth,120\n');

console.log('wrote:', file);
console.log(fs.readFileSync(file, 'utf8').trim());

fs.rmSync('reports-demo', { recursive: true, force: true });
console.log('cleaned up:', fs.existsSync('reports-demo'));

Joining can escape its base directory

The second row shows join() faithfully collapsing '..' straight out of the uploads directory. The output demonstrates why join() alone is not a containment check: you need the path.relative() test shown here to reject the escaping input.

Example · javascript
const path = require('node:path');

const root = 'uploads';
for (const userInput of ['avatars/me.png', '../../etc/passwd', './a/../b.txt']) {
  const joined = path.join(root, userInput);
  const escaped = path.relative(root, joined).startsWith('..');
  console.log(joined.padEnd(22), 'escaped root:', escaped);
}

Discussion

  • Be the first to comment on this lesson.