path.join vs path.resolve
Both methods of the path
module in Node are used to create paths; a typical use case is when you build an Express server and serve static files (e.g. the build of the front end of your application):
app.use(express.static(path.join(__dirname, 'A_PATH_COMES_HERE')));
But they are often used in other parts of a Node.js application as well.
Let’s have a look at some examples where we call these methods with the same arguments, so we can directly compare them.
Note that I’m writing this post on a Windows machine and the delimiter will always be \
. Linux and Mac will produce slightly different outputs.
The arguments
Both methods can accept optional string arguments, as many as you wish. Just for the record, if you don’t provide any argument, join
returns the current directory(.
) and while resolve
also gives back the directory from where the file was executed but it does it as an absolute path from the root:
path.join(); // .
path.resolve(); // C:\Users\USERNAME\FULL_PATH_TO_THE_FOLDER
This is a very important difference, which affects how these methods behave in various situations.
If you feed the methods with arguments other than strings, they will throw an error.
Slash or no slash?
The /
doesn’t seem to be a big issue but it makes a huge difference. If the first character of the path fragment entered as argument is a /
, an absolute path is immediately created:
path.join('hello'); // hello
path.resolve('hello'); // C:\Users\USERNAME\FULL_PATH_TO_THE_FOLDER\hello
path.join('/hello'); // \hello
path.resolve('/hello'); // C:\hello
join
converts the /
to the delimiter of your operation system.
resolve
, on the other hand, always returns an absolute path. With the /
in /hello
the absolute path is created and resolve
attach it to the root directory. It can be the C
(or D
or whatever) drive you run the script on Windows and the home
folder on Linux.
Multiple path fragments
Let’s challenge the methods further and call them with multiple path fragments:
path.join('hello', 'path'); // hello\path
path.resolve('hello', 'path'); // C:\Users\USERNAME\FULL_PATH_TO_THE_FOLDER\hello\path
path.join('/hello', 'path'); // \hello\path
path.resolve('/hello', 'path'); // C:\hello\path
This is the same situation as above. join
returns a concatenation of the path fragments and resolve
creates an absolute path.
What if we prepend the /
to the second path segment?
path.join('hello', '/path'); // hello\path
path.resolve('hello', '/path'); // C:\path
path.join('/hello', '/path'); // \hello\path
path.resolve('/hello', '/path'); // C:\path
As for join
, there’s no change. It diligently joins the path segments and returns the result.
But things are different for resolve
as the hello
(or /hello
, it doesn’t matter) segment disappeared. This is because resolve
creates paths from right to left and with the /
in /path
an absolute path can be created. The method will stop here and will completely ignore everything to the left.
This can go indefinitely:
path.join('hello', '/path', 'me'); // hello\path\me
path.resolve('hello', '/path', 'me'); // C:\path\me
path.join('/hello', '/path', '/me'); // \hello\path\me
path.resolve('/hello', '/path', '/me'); // C:\me
join
concatenates the the path fragments and resolve
looks for the first segment with /
from the right and append everything up to this point to the root.
Add dots
Both methods normalize the returned path, which means that they manage the ..
characters the way we normally use them for.
How do they handle it?
Let’s add ..
into the segments:
console.log('join: ', path.join('/..')) // \
console.log('resolve: ', path.resolve('/..')) // C:\
..
means that we have to go up by one level in the folder structure.
In the case of join
the response will be the delimiter itself (\
on Windows and /
on Linux and Mac).
resolve
, on the other hand, returns an absolute path, so when the first /
is found from the right (let’s not worry about trailing /
now), it creates the path from the root.
Add them to paths
Having said that let’s figure out what the responses are if the ..
is added to some path segments:
console.log('join: ', path.join('/hello', '/../path')) // \path
console.log('resolve: ', path.resolve('/hello', '/../path')) // C:\path
The /hello
(or just hello
, it doesn’t matter here) segment will be ignored because we go one level up from path
and this level will be the hello
segment itself. The /hello
(or the first path segment argument as is) and the /..
will simply cancel out.
As for join
, we step into hello
but then immediately step out of it by going one level higher (/..
), so the result will be /path
.
In the case of resolve
the path
segment will be attached to the root.
Similarly:
console.log('join: ', path.join('hello', '../path')) // path
console.log('resolve: ', path.resolve('hello', '../path')) // C:\Users\USERNAME\FULL_PATH_TO_THE_FOLDER\path
The difference is that we have no leading /
, so join
returns just path
, while resolve
attaches it to the full path from the root. One path segment (hello
in this case) and ..
will again cancel out.
Conclusion
join
and resolve
are two frequently used methods when it comes creating paths. The main difference is that join
concatenates (i.e. joins) the path segments and resolve
creates an absolute path from the root. Both methods will normalize the paths i.e. they treat ..
as we normally use them when navigating in the folder structure.
Thanks for reading and see you next time.