Node.js: refreshing a module using require.cache

Gemma BlackGemma Black
5 min read

This is purely in reference to CommonJS modules.

TLDR

Before I bore you with why I did this, refreshing a module can be done by deleting its reference in the require.cache object. Like so:

require('./some-module')

// use module

delete require.cache[require.resolve('./some-module')]

// use reloaded module

Simples. But there are caveats. But first.

Why was I interested in refreshing node modules?

If you have a small project, you can just use nodemon or node -—watch to watch for file changes, but it restarts the whole application from scratch.

  1. You lose state.

  2. You have to wait for the application to restart

Now I wasn’t worried about state. That’s not a major problem for me. But application start up time was.

I could try to improve application start up time, or, I could reload only the files I work on. But before I could use require.cache, I needed to understand a few core principles about it first.

Caching

Probably, old news, but Node caches modules you require. It’s great for performance as the module only needs to be loaded once. Let’s take the following CommonJS example.

We call console.log in our child.js file.

// child.js
console.log('hi', Date.now())

And let’s say we reference our child.js file in our main.js.

// main.js
require('./child')
require('./child')

If we run node main.js, we get our hi log with the timestamp:

hi 1731770123765

Notice, we’ve required child.js twice, but it only gets ran once.

This is because the module is loaded into require.cache. And the cached version is used on the second require.

And what does the cache look like?

[Object: null prototype] {
  '/Users/username/Workspace/project/00_required/main.js': {
    id: '.',
    path: '/Users/username/Workspace/project/00_required',
    exports: {},
    filename: '/Users/username/Workspace/project/00_required/main.js',
    loaded: false,
    children: [ [Object] ],
    paths: [
      '/Users/username/Workspace/project/00_required/node_modules',
      '/Users/username/Workspace/project/node_modules',
      '/Users/username/Workspace/node_modules',
      '/Users/username/node_modules',
      '/Users/node_modules',
      '/node_modules'
    ],
    [Symbol(kIsMainSymbol)]: true,
    [Symbol(kIsCachedByESMLoader)]: false,
    [Symbol(kIsExecuting)]: true
  },
  '/Users/username/Workspace/project/00_required/child.js': {
    id: '/Users/username/Workspace/project/00_required/child.js',
    path: '/Users/username/Workspace/project/00_required',
    exports: {},
    filename: '/Users/username/Workspace/project/00_required/child.js',
    loaded: true,
    children: [],
    paths: [
      '/Users/username/Workspace/project/00_required/node_modules',
      '/Users/username/Workspace/project/node_modules',
      '/Users/username/Workspace/node_modules',
      '/Users/username/node_modules',
      '/Users/node_modules',
      '/node_modules'
    ],
    [Symbol(kIsMainSymbol)]: false,
    [Symbol(kIsCachedByESMLoader)]: false,
    [Symbol(kIsExecuting)]: false
  }
}

But what if we want to invalidate the cache for our scenario - reloading on the files that have changed? That way we get the child.js console to run twice.

Well, we can just delete it.

Invalidating the cache

If we delete the cache before requiring the file again, we can get Node to re-load the file as it can’t find it in the cache.

// main.js
require('./child')

delete require.cache[require.resolve('./child')]

require('./child')

Then we can see our file has been reloaded and we get two console commands, with two different timestamps.

hi 1731749319540
hi 1731749319548

But can this help with only loading the files I need for development. Well, it depends.

But what are the caveats?

Things get a little interesting when you export functions or variables from the child file and reference them in the parent file through a variable. What do I mean?

First, imagine child.js now exports a function.

// child.js
module.exports = () => console.log('hi', Date.now())

Second, imagine the main.js not only requires the function but assigns it to a constant. Well, you cannot use a constant again in the same blocked scope:

// main.js
const child = require('./child')
child()

delete require.cache[require.resolve('./child')]

child = require('./child')
child()

You get this error:

hi 1731772783273
/Users/username/Workspace/project/08_required/main.js:6
child = require('./child')
      ^

TypeError: Assignment to constant variable.

You say, you can solve this by using let instead, but 1) not being able to use const isn’t a good solution if you’re using it for immutability and 2) even if you use let, you will need reassign the required variables. And I envisioned this getting rather messy.

But Jest deletes the cache without any problems!

Correct, but notice how they use it in their docs.

beforeEach(() => {
  jest.resetModules();
});

test('works', () => {
  const sum = require('../sum');
});

test('works too', () => {
  const sum = require('../sum');
  // sum is a different copy of the sum module from the previous test.
});

Notice how the required module is being required inside a function. It’s being lazy-loaded. So instead of being required and cached on application start up, it’s only required when the function runs.

For testing this is okay, performance is not as critical although waiting forever for tests aren’t fun either, but :

  1. In production, initial requests will get I/O latency before the required modules are cached.

  2. We’d need to use anonymous functions that could be refreshed to avoid figuring out how to reassign variables again.

  3. It would require changing how we write code to require modules in a function which goes against the conventions of how Nodejs was designed to be written.

So is lazy-loading is out of the question?

Well, you would have to require modules in functions like this.

// abc.js
module.exports = () => {
    console.log('abc2 module loaded at ', Date.now());
};

// child.js
module.exports = () => {
    const abc = require('./abc')
};

And someone achieved this in his ExpressJs development environment by being a little more clever about what exactly he lazy-loaded.

// server/index.js

const express = require("express");
const port = parseInt(process.env.PORT, 10) || 3000;

// ...

// File watcher could go here

// ...

const app = express();

//Hot reload!
//ALL server routes are in this module!
app.use((req, res, next) => {
   require("./app/router")(req, res, next);
});

//...

app.listen(port, err => {
  if (err) throw err;
  console.log(`> Ready on http://localhost:${port}`);
});

Notice:

  1. The initial request would have a slower load time due to needing to load require("./app/router"). But subsequent requests would be cached!

  2. And that he separated the router handling into its own file in an anonymous function.

Disclaimer: I haven’t tried his solution yet, I’ll leave that for another article.

So can we achieve some sort of hot-module-reloading in native Node.js

It’s possible but it’s awkward to achieve fully based on what I’ve investigated so far. It can be done however partially. Hopefully I’ll share that article soon.

0
Subscribe to my newsletter

Read articles from Gemma Black directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Gemma Black
Gemma Black

I'm a Senior Software Engineer. With 10+ years working within tech teams, and 20+ years working with code, I develop across the stack, assisting with application design, maintenance, deployment and DevOps within the AWS Cloud.