path.normalize()
Resolves '..' and '.' segments and collapses repeated separators in a path string.
path.normalize(path)- Added in Node.js v0.1.23
Parameters
| Parameter | Type | Description |
|---|---|---|
$path | string | The path to normalize. |
Return value
stringThe normalized path, or '.' if the input is a zero-length string.
Errors
Throws a TypeError (ERR_INVALID_ARG_TYPE) if path is not a string.
path.normalize() tidies a single path string: '.' segments are dropped, '..' segments cancel the segment before them, and runs of separators collapse into one. On Windows forward slashes are also converted to backslashes.
When to reach for it
Reach for it when you have a path assembled elsewhere — read from a config file, pasted by a user, built by string concatenation — and you want a canonical form before comparing it, logging it, or using it as a cache key. path.join() and path.resolve() already normalize their output, so calling it on their result is redundant.
What trips people up
It is purely lexical. It never touches the filesystem and knows nothing about symlinks, so 'a/symlink/..' normalizes to 'a' even though following the link on disk would land somewhere else. When you need the true on-disk path, use fs.realpath().
Trailing separators are preserved, so 'uploads/2024//05/' becomes 'uploads/2024/05/' with the final slash intact — a difference that breaks naive string equality between two paths pointing at the same directory.
Leading '..' segments on a relative path cannot be cancelled and survive: '../../etc/passwd' comes back unchanged. Normalizing is not sanitizing.
Example
When to use it
- Canonicalising paths read from a config file before comparing or deduplicating them
- Cleaning up a path built by string concatenation that ended up with double slashes
- Producing a stable cache key from paths that may be written inconsistently
Frequently asked questions
What does path.normalize actually do?
It resolves '.' and '..' segments and collapses repeated separators in a single path string, returning the tidied result. It is purely textual — it never reads the filesystem and does not make the path absolute. Both path.join() and path.resolve() apply it to their output already.
Does path.normalize prevent directory traversal?
No. Leading '..' segments on a relative path survive normalization untouched, so '../../etc/passwd' comes back exactly as it went in. To confine a path, resolve it against a root and confirm path.relative(root, target) does not begin with '..'.
Why does path.normalize keep the trailing slash?
Because the trailing separator is documented as preserved — it can carry meaning, such as asserting that a path names a directory. If that breaks string comparison for you, strip it yourself or run the path through path.resolve(), which removes trailing separators except at the root.
More examples
normalize() versus resolve()
Both collapse the same segments, but normalize() returns a relative string while resolve() anchors it to the current working directory and returns an absolute one. Pick normalize() when the path must stay relative.
const path = require('node:path');
const messy = 'a/b/../../../c';
console.log('normalize:', path.normalize(messy));
// -> ../c (still relative, leading '..' kept)
console.log('resolve: ', path.relative(process.cwd(), path.resolve(messy)));
// -> ../c (same shape, but resolve() produced an ABSOLUTE path first)
console.log('resolve is absolute:', path.isAbsolute(path.resolve(messy)));Trailing separators defeat equality checks
The two inputs name the same directory, yet normalize() keeps the trailing slash on the first and the strings differ. If you are comparing paths, strip trailing separators yourself or run both through path.resolve(), which removes them.
const path = require('node:path');
const a = path.normalize('srv//app/');
const b = path.normalize('srv/./app');
console.log(JSON.stringify(a), JSON.stringify(b));
console.log('equal:', a === b);
const strip = (p) => p.length > 1 ? p.replace(/[\\/]+$/, '') : p;
console.log('equal after stripping:', strip(a) === strip(b));Windows separators are unified
The win32 implementation accepts both slash styles as separators but only ever emits backslashes. The last line shows that '..' segments above an absolute root are simply discarded, unlike the relative case where they are preserved.
const path = require('node:path');
console.log(path.win32.normalize('C:////temp\\\\/\\/\\/foo/bar'));
// -> C:\temp\foo\bar
console.log(path.win32.normalize('C:\\temp\\\\foo\\bar\\..\\'));
// -> C:\temp\foo\
console.log(path.posix.normalize('/a/../../b'));
// -> /b (an absolute path cannot go above its root)
Discussion