To Resolve or To Join?

Last Updated: March 14, 2020.

If you’ve used Node.js for more than at least half a year, I’m fairly certain you might have come across the built-in path module.

In this note we’ll be looking at two APIs the path module provides us with which, on the surface, seem to do similar duties.

These APIs?

path.resolve && path.join

Let’s dive in…

What For Though?

Before looking at the similarities and differences between both methods, it’s worth pointing out that most of the time, my use case for reaching out for any of them is to get the absolute path of a file… a file I’m either trying to read from, write to, delete, or manipulate in some form or another.

Here’s what I mean…

Say we have the following relative directory structure…

...
├── africa
│   └── rwanda
│       └── cities.json
├── asia
└── europe
    └── norway
        └── index.js

… the current file we are executing is europe/norway/index.js and from within that file we want to get the absolute path to africa/rwanda/cities.json, we can make use of any of the two path APIs.

For example using path.join, we can have something along the lines of:

const absPath = require('path').join(
  __dirname,
  '../..',
  'africa/rwanda/cities.json'
);

console.log(absPath); // /Users/kizito/snippets/continents/africa/rwanda/cities.json

path.join

Per the spec, path.join accepts a sequence of path segments as arguments and joins them using the platform-specific delimiter (i.e. for windows \ while for most *nix based /) then normalizes the resulting path.

In simple terms (with an example), say we pass the following segment sequence into the method

'path', 'to', 'another', '..', 'another', 'file'

The resulting joined and normalized path on Linux/Mac would be:

'path/to/another/file'

while on Windows…

'path\to\another\file'

The “join” part of the spec description simply concatenates the provided segments with the platform-specific delimiter, while the “normalize” part resolves the .. and (if available) . segments.

Normalization is the reason why instead of getting this as the output…

'path/to/another/../another/file'

… we got the previous value.

path.resolve

path.resolve returns an absolute path when called with path segments as arguments.

However, unlike path.join which joins and normalizes all provided segment sequences from left to right in the order the arguments were passed, path.resolve concatenates by prepending the provided segments from right to left until an “absolute path” is found.

Let’s break down the resolve part of that statement with an example. Say we have the following path segments:

'/how', 'to', 'code'

this would resolve to …

'/how/to/code'

How?

Like so…

First, the right-most argument is taken…

'code'

then, the argument preceding that is prepended to it using the proper platform delimiter (assuming Linux)

'to/code'

finally, the argument preceding the previous is also prepended

'/how/to/code'

Worth noting is that passing the following segments …

'aubrey', '/drake', 'graham'

would resolve to …

'/drake/graham'

Why?

Because while prepending, the first absolute path found was /drake. So the method resolved with the formed path so far.

Similarly, 'jermaine', 'lamarr', '/cole' would resolve to …

'/cole'

If no absolute path is found in the provided segments while resolving, the segments resolved so far would be prepended with the absolute path of the directory containing the file or module being executed. This implies that resolving 'jhene', 'aiko' will produce …

'/Users/kizito/snippets/jhene/aiko'

… assuming the code is executed from a file in a snippets directory on my home folder (i.e., ~/snippets/file.js).

Also, just calling path.resolve() without passing any path segments would resolve to the absolute path of the current working directory.

So, To Resolve or To Join?

Honestly, it’s down to personal preference.

I use path.join in conjunction with the built-in __dirname variable (which is the directory name of the current module) because I find it more explicit.

That itself, however, is similar to using path.resolve, passing in path segments that are not “absolute” (i.e segments not starting with /).

So, use whichever you and your team are most comfortable with.