path.join vs path.resolve

Node.js has a built-in path module, which can be used for creating file and directory paths in our application. Two methods of this module are join() and resolve(), which serve the similar purpose of creating paths. Let's see these methods in detail in a beginner friendly way.

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.