Alternatives to __dirname in Node.js with ES modules
Tags: node
Published: 2022-03-11 17:17:14

**Written by [Sam Thorogood](https://blog.logrocket.com/author/samthorogood/)✏️**

So, you've listened to all the advice and sat down to migrate your code or learn a new standard, but you have questions. ES modules (also known as ESM) are here, but using them is not quite as simple as migrating all your `require()` expressions into `import` statements.

ES modules were added to Node in Node 13, about the end of 2019\. And Node 12—the last version _without_ ESM—is set for "end of life" in April of 2022, so: presuming your systems are being upgraded, there'll be fewer and fewer places without native support.

*   [Help, I’m missing `__dirname`](#help-im-missing-dirname)
*   [How does getting `__dirname` back work?](#how-does-getting-`dirname`-back-work)
*   [What is your goal?](#what-is-your-goal)
    *   [Goal: Load a file](#goal-load-a-file)
    *   [Goal: Dynamically import code](#goal-dynamically-import-code)
    *   [Goal: Performing path-like operations and gotchas](#goal-performing-path-like-operations-and-gotchas)
*   [Interoperability between `URL` and `path` strings](#interoperability-between-`url`-and-`path`-strings)
*   [Final thoughts](#final-thoughts)

## Help, I'm missing `__dirname`

Yes! This is the point of the post.

If you're writing an ES module with the `mjs` extension (which forces Node into ESM mode), or with `{"type": "module"}` set in your `package.json` file, or you're writing TypeScript and running code some other way… you might encounter this error:

```js
    ReferenceError: __dirname is not defined in ES module scope 
```

Similarly, other inbuilt globals that were provided to CommonJS code won't exist. These are `__filename`, `exports`, `module`, and `require`.

To get `__dirname` (and `__filename`) back, you can add code like this to the top of any file that needs it:

```js
    import * as url from 'url';
    const __filename = url.fileURLToPath(import.meta.url);
    const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
```

Great!

## How does getting `__dirname` back work? Any caveats?

I'm glad you're reading on! The above code works because ESM provides a new, standardized global called `import.meta.url`. It's available in all browsers and Node when running module code, and it will be a string like:

`"file:///path/to/the/current/file.js"`

```js
    "file://C:\Path\To\current\file.js"   // windows without WSL
    "https://example.com/source.js"   // if this is browser JS
```

This brings Node inline with ESM in your browser. As JS developers, we need this new global because our code may run anywhere, locally or remote, and the standard URL format gives us support for that. Of course, you might remember that Node can't directly import from a web URL, but new tooling like Deno can.

The new `__dirname` and `__filename` variables created in the code above work just like in CommonJS — if you pass them around, they'll still have the string name of the original file. They're not variables that suddenly take on the role of pointing to the directory or filename. (This is a long way of saying you probably don't want to `export` them.)

But note that while the helper above, `fileURLToPath`, is a quick solution if you're just trying to upgrade old code, note that it's not standardized and won't work if, e.g., your code is shared with the web.

To be fair, this is not really a new problem: `__dirname` and `__filename` aren't shared either, but `import.meta.url` is! So, using it directly (read on!) actually lets us be more versatile.

## What is your goal?

Why is it useful to have `__dirname` and `__filename` within our scripts?

It's to be able to interact with the world around our code. These are helpful to import other source files, or to operate in a path that is related to our path.

For example, maybe you've got a data file that lives as a peer to your code ("yourprogram.js" needs to import "helperdata.txt"). And this is probably why you want `__dirname` over `__filename`: it's more about where your file is rather than the file itself.

But! It's possible to use the inbuilt object `URL`, and many of Node's inbuilt functions, to achieve a variety of goals without having to simply pretend like we're building CommonJS code.

Before we start, note a few oddities:

*   URLs are mutable, and we create a new one by passing (a) a string describing what's changed and (b) a previous `URL` instance to base off. (The order, with the smaller changed part first, can trip people up)
*   The `import.meta.url` value isn't an _instance_ of `URL`. It's just a string, but it can be used to construct one, so all the examples below need us to create new objects

There are a couple of reasons for `import.meta.url` being a simple string, one of which is that a `URL` [is mutable](https://developer.mozilla.org/en-US/docs/Glossary/Mutable). And we have JS's legacy on the web to thank—if you change `window.location.pathname`, you're modifying the page's URL to load a new page.

In that way, `window.location` itself remains the same object. And in an ES Module, “changing” the URL makes no sense—the script is loaded from one place and we can't redirect it once that's happened.

_N.B., `window.location` isn't actually a URL, but it acts basically like one._

### Goal: Load a file

We can find the path to a file in the same directory as the file by constructing a new URL:

```js
    const anotherFile = new URL('helperdata.txt', import.meta.url);
    console.info(anotherFile.toString());  // prints "file:///path/to/dirname/helperdata.txt"
```

Okay, that's great, but you might point out: I still have a `URL` object, not a string, and it still starts with `file:///`.

Well, the secret is that Node's internal functions will actually handle a `file://` just fine:

```js
    import * as fs from 'fs';
    const anotherFile = new URL('helperdata.txt', import.meta.url);
    const data = fs.readFileSync(anotherFile, 'utf-8');
```

Great! You've now loaded some data, without resorting to the `path` helper library.

### Goal: Dynamically import code

Just like with reading an adjacent file, we can pass a `URL` into the dynamic `import()` helper:

```js
    const script = 'subfolder/other.mjs';
    const anotherScript = new URL(script, import.meta.url);
    const module = await import(anotherScript);
```

Again, we have a `URL` object, which is happily understood by `import`.

### Goal: Performing path-like operations and gotchas

The URL object works a bit differently than `path` helpers when it comes to finding the current directory or navigating folders. The `path.dirname` helper is a good example of this — it roughly means “find me the parent path to the current path.” Read on:

```js
    path.dirname('/home/sam/testProject/')   // '/home/sam/'
    path.dirname('/home/sam/testProject')    // '/home/sam/'
    path.dirname('/home/sam/')    // '/home'
```

Importantly, note above that `path` doesn't really care about the trailing `/` — it only cares if there's something after it.

To perform a similar operation on a URL, we add the strings `.` or `..` (meaning “go up a directory”), but it has subtly different outcomes than `path.dirname`. Take a look:

```js
    // if import.meta.url is "/my/src/program.js"
    const dirUrl = new URL('.', import.meta.url);  // "file:///my/src/"
    const dirOfDirUrl = new URL('.', dirUrl);  // "file:///my/src/" - no change
    const parentDirUrl = new URL('..', import.meta.url);  // "file://my/"
    const parentDirOfDirUrl = new URL('..', dirUrl);  // "file://my/" - same as above
```

What we've learned here is that `URL` cares about the trailing slash, and adding `.` to a directory or a file in that directory will always give you a consistent result. There's similar behavior if you're going down into a subfolder:

```js
    const u1 = new URL('subfolder/file.txt', import.meta.url);   // "file:///my/src/subfolder/file.txt"
    const u1 = new URL('subfolder/file.txt', dirUrl);   // "file:///my/src/subfolder/file.txt"
```

I think this is much more helpful than what the inbuilt features to Node `path.dirname` and so on do — because there's a strong distinction between file and directory.

Of course, your view might differ — maybe you want to get back to simple strings as fast as possible — and that's fine, but it's worth understanding `URL`'s semantics. It's also something that we have available to us on the web, and these rules all apply to `https://` schemes just as much as they do to `file://`.

## Interoperability between `URL` and `path` strings

As much as I want to educate you on how `URL` works and all its nuances, we as developers who might be interacting with the file system will always eventually want to get back to pure, simple `path` strings — like "/Users/Sam/path/to/your/file.js". You can't (easily) use `URL` to generate relative paths between files, like with `path.relative`, and URLs themselves must be absolute (you can't work on unrooted paths like "relative/path/to/file.js").

You might know that URLs have a property called `pathname`. On the web, this contains the part after the domain you're opening. But for `file://` paths, this contains the whole path — e.g., `file:///path/to/file` would be "/path/to/file".

But wait! Using this directly is actually dangerous for two reasons, which is why at the top of this post I talk about using Node's inbuilt helper `url.fileURLToPath`. This solves two issues for us:

*   Spaces in filenames won't work with `pathname` — on the web, they're encoded as `%20`, which your computer doesn't understand
*   Windows paths aren't normalized with `pathname`

So resist the urge to just use a URL's `pathname` and use the helper that I introduced all the way at the top of the file:

```js
    const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
    // or
    const pathToFile = url.fileURLToPath('file:///some/path/to/a/file');
```

## Final thoughts

In writing this up, I had a couple of thoughts which didn't really fit anywhere else:

*   Node in ES Module mode still provides `process.cwd()`, and this is just a regular path—like "/foo/bar" — it's not now a `file:///foo/bar/` just because you're in module mode
*   You can convert from a string back to a URL with the `url.filePathToURL` helper — it works in reverse. But, you probably won't need to do this as often

Thanks for reading! Hit me up on [@samthor](https://twitter.com/samthor) if you have any questions.

---
##200’s only :heavy_check_mark: Monitor failed and slow network requests in production

Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third party services are successful, [try LogRocket.](https://logrocket.com/signup/)

![LogRocket Sign Up](https://blog.logrocket.com/wp-content/uploads/2019/12/network-request-filter-2-1.png)https://logrocket.com/signup/

[LogRocket](https://logrocket.com/signup/) is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. [Start monitoring for free.](https://logrocket.com/signup/)