The Hidden World of require(): Module Wrapper Function, module.exports, and exports
In the previous article, we learned why modules exist.
We discovered that every JavaScript file in Node.js is treated as an independent module with its own private scope.
But a much bigger question remains.
When we write:
const math = require("./math");
what actually happens?
Does JavaScript understand require()?
Does the V8 Engine load another file?
Or does Node.js perform some hidden magic?
Let's open the black box.
Is require() a JavaScript Feature?
One of the biggest misconceptions among beginners is this:
"require() is part of JavaScript."
It isn't.
Open your browser console and type:
require("./math");
You'll get something similar to:
ReferenceError: require is not defined
Why?
Because JavaScript itself has no built-in function called require().
The ECMAScript specification—the official definition of JavaScript—doesn't include it.
So where does it come from?
The answer is simple:
require() is created by Node.js, not by JavaScript.
It is part of the Node.js runtime.
Then How Can We Use It in Every File?
This is where Node.js does something brilliant.
Suppose you create a file called:
math.js
containing:
const PI = 3.14159;
function add(a, b) {
return a + b;
}
You wrote only these few lines.
But Node does not execute this file exactly as it appears.
Before execution, Node wraps your code inside another function.
Conceptually, it becomes:
(function (exports, require, module, __filename, __dirname) {
const PI = 3.14159;
function add(a, b) {
return a + b;
}
});
This hidden function is called the Module Wrapper Function.
Node creates it automatically for every CommonJS module.
You never write it yourself.
Why Wrap Every Module?
At first, wrapping every file may seem unnecessary.
But this single design solves multiple problems.
It creates:
A private scope
Module isolation
Access to require
Access to module
Access to exports
Access to __filename
Access to __dirname
Without this wrapper, every variable would become global.
Imagine two files.
const PORT = 3000;
and
const PORT = 5000;
Without isolation, these variables would collide.
Because of the wrapper, each file gets its own execution environment.
Understanding the Wrapper Parameters
Let's examine the hidden function carefully.
(function (
exports,
require,
module,
__filename,
__dirname
) {
// Your code
});
Every parameter has a purpose.
- require const fs = require("fs");
require() loads another module.
Node creates this function before your code starts executing.
It is not provided by V8.
- module
Every file gets a module object.
Conceptually:
module = {
exports: {}
}
Initially,
module.exports
is simply an empty object.
- exports
Another interesting parameter appears.
exports
Many beginners think
exports
and
module.exports
are different objects.
Initially...
they are references to the same object.
Conceptually:
exports
│
▼
{}
▲
│
module.exports
Both variables point to the same object.
Example
exports.name = "Sudhanshu";
Internally this becomes
module.exports.name = "Sudhanshu";
because both references currently point to the same object.
When Do They Become Different?
Suppose you write
exports = {};
Now
exports
points somewhere else.
But
module.exports
still points to the original object.
Visualization:
Initially
exports
│
▼
{}
▲
│
module.exports
After reassignment
exports
│
▼
{}
module.exports
│
▼
{ name: "Sudhanshu" }
They are no longer connected.
This is why Node returns module.exports, not exports.
We'll revisit this in even more detail later.
- __filename
Node automatically provides the absolute path of the current module.
Example:
console.log(__filename);
Output:
C:\Projects\BankApp\math.js
Useful for logging and locating resources.
- __dirname
Similarly,
console.log(__dirname);
might produce
C:\Projects\BankApp
This is extremely useful when loading files relative to the current module.
What Happens When We Call require()?
Suppose our project looks like this.
project/
├── app.js
└── math.js
math.js
function add(a, b) {
return a + b;
}
module.exports = add;
app.js
const add = require("./math");
console.log(add(2,3));
Now let's follow the execution.
Step 1
Node begins executing
app.js
inside its own wrapper function.
Step 2
Execution reaches
require("./math")
Node recognizes that another module is required.
Step 3
Node resolves the path.
It asks:
Where is "./math"?
Node tries to locate the corresponding file.
Eventually it finds
math.js
Step 4
Node reads the contents of the file.
At this point,
it has only plain text.
function add(a,b){
return a+b;
}
module.exports = add;
Nothing has executed yet.
Step 5
Before execution,
Node wraps the file.
Conceptually:
(function(exports, require, module, __filename, __dirname){
function add(a,b){
return a+b;
}
module.exports = add;
});
Notice that the wrapper is created before V8 executes the module.
Step 6
Now V8 executes the wrapped function.
During execution,
Node updates
module.exports
to reference
add
At the end of execution,
the module object looks conceptually like:
module = {
exports: add
}
Step 7
require() returns
module.exports
Therefore
const add = require("./math");
becomes conceptually equivalent to
const add = module.exports;
Now
add(2,3)
works exactly as expected.
The Complete Flow
Let's summarize everything.
app.js
↓
require("./math")
↓
Resolve File Path
↓
Read File
↓
Wrap Inside Module Function
↓
Create module object
↓
Create exports reference
↓
Provide require()
↓
Provide __filename
↓
Provide __dirname
↓
Execute with V8
↓
Populate module.exports
↓
Return module.exports
↓
Continue app.js
This entire process happens every time a new CommonJS module is loaded.
Why Doesn't V8 Do This?
Because none of this belongs to JavaScript.
V8 understands:
Variables
Functions
Arrays
Objects
Loops
Classes
It does not understand:
Files
Modules
require()
module.exports
__dirname
Those are runtime features provided by Node.js.
This distinction is extremely important.
Key Takeaways
After reading this article, you should understand:
require() is not part of JavaScript.
Node.js injects require() into every CommonJS module.
Every module is wrapped inside a hidden function before execution.
The wrapper creates a private scope.
module.exports is the object that Node actually returns.
exports is initially just another reference to module.exports.
Reassigning exports does not change module.exports.
V8 executes the wrapped function, but the wrapper itself is created by Node.js.
Coming Next
In Part 4.3, we'll go even deeper.
We'll answer questions such as:
How does Node know where to find a module?
Why does require("./math") work without writing .js?
How does Node search node_modules?
What is the Module Resolution Algorithm?
Why is require() synchronous?
What is Module Cache, and why is a module executed only once?
How do Circular Dependencies work internally?
By the end of Part 4.3, you'll understand the entire lifecycle of a CommonJS module—from the moment you type require() until Node returns the exported value.
Top comments (0)