Search code examples
javascriptnode.jscommonjs

Object.assign(module.exports, {...}) vs module.exports = {...}


Can someone explain with an example how module.exports = {...} will cause unexpected behavior.

I'm reading you don't know js yet and I came across this at https://github.com/getify/You-Dont-Know-JS/blob/2nd-ed/scope-closures/ch8.md#node-commonjs-modules

Some developers have the habit of replacing the default exports object, like this:

// defining a new object for the API
module.exports = {
    // ..exports..
};

There are some quirks with this approach, including unexpected behavior if multiple such modules circularly depend on each other. As such, I recommend against replacing the object. If you want to assign multiple exports at once, using object literal style definition, you can do this instead:

Object.assign(module.exports,{
   // .. exports ..
});

What's happening here is defining the { .. } object literal with your module's public API specified, and then Object.assign(..) is performing a shallow copy of all those properties onto the existing module.exports object, instead of replacing it This is a nice balance of convenience and safer module behavior.


Solution

  • The exports object is created for your module before your module runs, and if there are circular dependencies, other modules may have access to that default object before your module can fill it in. If you replace it, they may have the old, original object, and not (eventually) see your exports. If you add to it, then even though the object didn't have your exports initially, eventually it will have it, even if the other module got access to the object before those exports existed.

    More in the Cycles section of the CJS module documentation.

    We can adapt the cycle example in that section to demonstrate it:

    a.js (note changes):

    console.log('a starting');
    // Removed: `exports.done = false;`
    const b = require('./b.js');
    console.log('in a, b.done = %j', b.done);
    exports = {done: true}; // ** Modified this line
    console.log('a done');
    

    b.js (unchanged):

    console.log('b starting');
    exports.done = false;
    const a = require('./a.js');
    console.log('in b, a.done = %j', a.done);
    exports.done = true;
    console.log('b done');
    

    main.js (unchanged):

    console.log('main starting');
    const a = require('./a.js');
    const b = require('./b.js');
    console.log('in main, a.done = %j, b.done = %j', a.done, b.done);
    

    When you run that:

    main starting
    a starting
    b starting
    in b, a.done = undefined
    b done
    in a, b.done = true
    a done
    in main, a.done = undefined, b.done = true
    (node:2025) Warning: Accessing non-existent property 'done' of module exports inside circular dependency
    (Use `node --trace-warnings ...` to show where the warning was created)
    

    Side note: Cycles are handled differently with JavaScript's own module system (ESM), and since there's no exports equivalent object (that you can access; there is conceptually), this issue doesn't arise. I recommend using ESM where possible. Node.js has supported it in a fairly stable (though still evolving) fashion since v12.