The difference between exports and module.exports

Another confusing concept in Node.js is the difference between module.exports and exports. Are they the same? Can I use exports the same way as I would use module.exports? If not, what is the difference? In this post, I will attempt to answer these questions.

When we write some code in a file which needs to be reused somewhere else, we’ll create a module and export the relevant piece of code.

The traditional and well-working way of using code in another files of the application is to require the module. Before the code of the required module is executed, Node.js wraps it in a function like this:

// wrapper function
(function(exports, require, module, __filename, __dirname) {
  // module code
});

As it can be seen, this wrapper function has a module and an exports arguments with both of which being local (i.e. specific) to the file itself.

They are not part of the global object. Although global has a module property, global.module === module will return false if we try to log it to the console. They are not the same. The module in question is specific to the file that is required.

Now that we see how both module and exports are generated, we can have a closer look at them.

module.exports

module is defined as an object and has a property called exports. The exports property is equal to an empty object by default.

To prove this, we can create a file called main.js and write a single line of code in it:

// main.js
console.log(module);
// returns an object with some properties, one of them being exports

Add properties to module.exports

We can add properties to the module.exports object, just like we can do the same with any JavaScript object literal:

// bond.js
module.exports = {
  myFunc: () => {
    // code here
  },
  name: 'James Bond'
}

Here we created two properties in the module.exports object: myFunc, whose value is a function and name, which is, of course, James Bond. What else could it be?

Both properties will be accessible in other files after we require this file.

Assuming that bond.js is in the same folder as the file where we need it (let’s call this other file main.js for now), we can do the following at the top of main.js:

// main.js
const myModule = require('./bond.js);

// call myFunc:
myModule.myFunc();

// log Mr Bond:
console.log(myModule.name); // James Bond

So far so good, nothing unexpected has happened.

Reassign module.exports

Because in this case exports is a property of module, it’s OK to make this property a function (or even a class):

// bond.js
module.exports = () => {
  console.log('My name is Bond, James Bond.');
}

We reassign the default exports property here and instead of being an object it will be a function.

In this case we need to invoke it in the file where we want to use it:

// main.js
const sayBond = require('./bond.js);

sayBond(); // My name is Bond, James Bond.

We can safely reassign module.exports because we don’t change the behaviour of module itself.

exports

On the other hand, we can’t do the same with exports.

As we have seen in the module wrapper, exports is another file specific argument just like module. When this wrapper function is invoked upon require-ing the file somewhere else, the value of exports is made equal to module.exports by default:

// something like this happens inside require()
// ...
const module = { exports: {} };
(function(module, exports) {
  // code here
})(module, module.exports);
// ...

This means that initially module.exports and exports are exactly the same. exports is just an alias or shorthand for module.exports, hence it defaults to an empty object.

As such, we can add properties to it just like we did above with module.exports:

exports.myFunc = () => {
  // function code
}; // we can do this, myFunc is a property of the exports object

But, we cannot reassign exports to something else, i.e. the following code does not work in the way we might expect:

exports = () => {
  // function code
}; // we CANNOT do this, a new variable called exports has been created

Why? Because here we create a new variable called exports and override the original one coming from the wrapper function.

In this case, exports will no longer be bound to module.exports, therefore whatever we assign exports, it won’t be exported.

Reassigning exports can lead to nasty and hard-to-find bugs, so this practice should be avoided.

Conclusion

module.exports and exports serve the same purpose as they are exactly the same. They can be used for each other without any problems with one caveat.

exports cannot be reassigned because in that case a new variable of the same name will be created. We can, however, safely give module.exports a new value.

Thanks for reading and I hope that the post was useful. If so, see you next time.