Files are only required once in Node.js

7 min read

Files are only required once in Node.js Banner

As a beginner, when you learn to write Node.js code, you end up writing all the code in a single file. The code contains mostly the basic stuff like declaring and using variables, conditionals, loops and functions just to understand the language. Later you learn how to build a server in Node.js, handle routes, write logic for specific routes and send responses. You realise that the only file you've been working on is getting bigger and bigger and its hard to keep track of things.

Thats where the modules come to rescue. The code now can be written in separate files and then later can be imported in the required files. This helps to structure our code and also reuse it. For this, we've been using the require() function in Node. If we want to use code from some file we just use the require() function to import the contents from it, assign it to a variable and use it.

What most people think about require()

To learn the module system, some may have watched tutorials online or read blogs about it. So, a require() statement takes the content of the given file and stores it in a variable (if assigned). But, the one thing that we're also been told is that the require() statement also runs the file. Meaning, if the file executes some function in it will be executed when the file is required.

This is where people develop a thought about how require() statement works. People think that the file is executed every time we require it. This is what they learnt, right? The require() statement executes the file when required. And its true, but not entirely. Let's see how.

The reality

Let's say we have two files - index.js and counter.js. The counter.js file contains a variable initialized to 0 and it exports two functions - getCounter and incrementCounter.

counter.js
let counter = 0;

module.exports = {
    getCounter() {
        return counter;
    },
    incrementCounter() {
        ++counter;
    }
};

Now, let's require this file in our index.js.

index.js
const counter = require('./counter');

console.log(counter.getCounter()); // 0

counter.incrementCounter(); // Increments to 1
counter.incrementCounter(); // Increments to 2
counter.incrementCounter(); // Increments to 3

console.log(counter.getCounter()); // 3

Initially, the counter will be 0. Then we increment it 3 times and the value of counter becomes 3. This is all expected. But, let's require the file one more time and store it in separate variable and try to log the counter variable again.

index.js
const counter = require('./counter');

console.log(counter.getCounter()); // 0

counter.incrementCounter(); // Increments to 1
counter.incrementCounter(); // Increments to 2
counter.incrementCounter(); // Increments to 3

console.log(counter.getCounter()); // 3

const counter2 = require('./counter');
console.log(counter2.getCounter()); // 3

Now, we require the counter.js file second time. So, it will run the file again. In that case, the counter variable will again be set to 0. The counter2.getCounter() must return 0. Surprisingly, it returns 3. How? We just required the file again so it must reset the variable to 0 for counter2. Thats where Node's require cache comes into play.

The require() function caches the required file and when it sees another require statement for the same file, rather than requiring the file and executing it again, it just returns the cached version. Because of this, the file is never executed second time in our example and the value of counter remains 3.

Although it's true that files are executed whenever they are required, but the reality is that they are only executed once even if you require them multiple times in multiple files.

A complex example (optional)

Let's see a more complex example. This is just for demo, its fine if you don't understand this as there is very rare chance that you will face this issue. Now, lets get started.

We'll create 4 files - index.js, counter.js, testFile1.js and testFile2.js which will have the following contents.

counter.js
let counter = 0;

module.exports = {
    getCounter() {
        return counter;
    },
    incrementCounter() {
        ++counter;
    }
};
testFile1.js
const counter = require('./counter');
require('./testFile2');

console.log('----- Test File 1 Start -----');
console.log(counter.getCounter());

counter.incrementCounter();
counter.incrementCounter();
counter.incrementCounter();

console.log(counter.getCounter());

console.log('----- Test File 1 End -----');
testFile2.js
const counter = require('./counter');

console.log('----- Test File 2 Start -----');
console.log(counter.getCounter());

counter.incrementCounter();
counter.incrementCounter();
counter.incrementCounter();

console.log(counter.getCounter());

console.log('----- Test File 2 End -----');
index.js
const counter = require('./counter');
console.log('----- Index File Start -----'); console.log(counter.getCounter()); counter.incrementCounter(); counter.incrementCounter(); counter.incrementCounter(); console.log(counter.getCounter()); const counter2 = require('./counter'); console.log(counter2.getCounter()); require('./testFile1'); require('./testFile2'); console.log(counter2.getCounter()); console.log('----- Index File End -----');

When you run index.js this is the following output you get. Let's see what it really means. The index file requires the counter.js file. Later, it console logs ----- Index File Start -----. Initially, the counter is 0, so the first getCounter() statement returns 0. We then increment the counter 3 times and we get the value 3. This is what everyone expects till now.

Now, we again require the counter file. As we now know that files are cached, we expect to see the returned value to be 3 and thats what we get.

Let's require another file, that is testFile1.js. As we can see it also requires the counter.js file. Below that, it requires the testFile2.js. Now testFile2.js was never required till now so node will execute this file. There we can see counter.js was required again. As counter.js is cached, testFile2.js will have the value of counter variable as 3 (as it was incremented 3 times already in index.js). So it will again increment the value of the counter variable 3 times and now we have counter equal to 6.

Requiring testFile2.js was at very top of the testFile1.js. That means we still need to run testFile1.js. We do the same thing that we did in testFile2.js. But, the value of counter has been changed to 6 by testFile2.js. So even if we've required counter.js above the testFile2.js, the value of counter variable still changed. Now, testFile1.js also increments counter by 3 which means the value of it is now 9.

Its not over yet. This was only the require('./testFile1') statement we addressed in index.js till now. That means the next statement to be executed will be require('./testFile2'), that is to require testFile2.js. But the file was already been required in testFile1.js, so node will return the cached version of testFile2.js. That means it won't run again. Which in turn means it won't call the incrementCounter() function 3 times and the value of counter remains 9. We can see that in the last log of index.js file.

index.js
Output of index.js

----- Index File Start -----
0
3
3
----- Test File 2 Start -----
3
6
----- Test File 2 End -----
----- Test File 1 Start -----
6
9
----- Test File 1 End -----
9
----- Index File End -----

This example might have given you even better idea of how require() works. You can even check the cache by console logging require.cache. It will log the cached files with some more data.

Even works with import statements

This also works with the import statements. You can even try by changing the require() statements to import. You'll have to setup babel so that you can use the import statements. Also, import statements needs to be declared at the top of the file. Which means the following code is not valid.

index.ts
import counter from './counter';

console.log('----- Index File Start -----');
console.log(counter.getCounter());

counter.incrementCounter();
counter.incrementCounter();
counter.incrementCounter();

console.log(counter.getCounter());

import counter2 from './counter';
console.log(counter2.getCounter());

Node will complain about the given line. Just move the import statements at the top and the results will be same as we saw.

Final thoughts

This is something that you won't run into everyday. You might not even have any issue because of this in your whole career. It is still good to know the internal workings of anything to be a better developer.