Skip to content

Instantly share code, notes, and snippets.

@pmuellr
Created December 15, 2013 16:43
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pmuellr/7975152 to your computer and use it in GitHub Desktop.
Save pmuellr/7975152 to your computer and use it in GitHub Desktop.
// module a
b = require("./b")
module.exports = function() {
console.log("I'm in module a")
}
b()
// module b
a = require("./a")
module.exports = function() {
a()
}

In node.js, you need to be careful when you export functions out of a module using the module.exports = style of export.

Try re-creating the two files a.js and b.js on your computer now, and then run node a. You should see an error like the following:

b.js:4
    a()
    ^
TypeError: Property 'a' of object #<Object> is not a function
    at module.exports (b.js:4:5)
    at Object.<anonymous> (a.js:7:1)
    at Module._compile (module.js:456:26)
    at Object.Module._extensions..js (module.js:474:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:312:12)
    at Function.Module.runMain (module.js:497:10)
    at startup (node.js:119:16)
    at node.js:901:3

How does this happen? Let's walk the steps.

Node loads module a because you specified loading it on the command-line. Internally, node creates a new empty object {} for the exports for the object; this object will actually be the value of the exports variable when module a is run.

The first thing module a does is load module b. So, let's move to module b.

Again, node will create a new empty object {} for the exports for the module. The first thing module b does is to require module a. Node is actually still in the process of loading module a - it hasn't finished - but it did already create the exports for module a, and so it returns them. At this point, the variable a in module b will be set to the new empty object {}. When a is invoked in the exported function below this, the error will result.

You can see in module a that it's supposed to be exporting a function. The problem is that this isn't done before module b does a require() on a, so b will end up getting access to the empty module export initially set for a.

@mmocny
Copy link

mmocny commented Dec 15, 2013

Patrick, I've also seen module.exports = { foo:.. bar:.. } instead of exports.foo = exports.bar =. I think the same issue will apply? Basically, don't override the exports object is the lesson here, I think?

@ricardobeat
Copy link

@mmocny the issue shown will happen whether you overwrite exports or not.

The "solution" is to avoid side-effects from module loading, that is, don't execute code from within a module when it's loaded, and minimize/plan your dependencies. For example, in this case, require('./b')() from a third file works fine. Some people (myself included) prefer to always overwrite module.exports at the end of the file, also preventing a partially exported module to be loaded.

@sintaxi
Copy link

sintaxi commented Dec 16, 2013

The anti-pattern I'm seeing is that module a is requiring module b AND module b is requiring module a. Its a recursive dependency semantically speaking.

@isaacs
Copy link

isaacs commented Dec 16, 2013

Easily worked around. Take advantage of function hoisting.

// module a

module.exports = fn

var b = require("./b")

function fn() {
    console.log("I'm in module a")
}

b()
// module b

var a = require("./a")

module.exports = function() {
    a()
}

@pmuellr
Copy link
Author

pmuellr commented Dec 16, 2013

Ya, I've only seen this "problem" in mutually require()'d modules, and it's fixed by assigning your module.exports before doing other require()'s (or the require()'s that cause the problem anyway).

In practice, I've seen this is in some of my packages where I try doing things like "every module is a class!", etc. These things typically don't survive very long. :-) And it's can be painful to deal with superclasses this way as well, since then you need to require() the superclass early (for CoffeeScript).

@pmuellr
Copy link
Author

pmuellr commented Dec 16, 2013

I'm trying to remember, in some of my old, early CommonJS experiments, if I had code to handle this. I thinking the best I could do is provide a warning, in cases where I recognized mutually recursive require()'s. Not helpful, because you don't want to see this "warning" all the time, so you would need to have folks opt-in for it, and then no one is going to use it.

I guess it's moot if no one ever sees this in practice. I will note that when I started seeing this myself, I was often confused as to what was going on. So even if it's a rare problem, it's a difficult to diagnose one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment