DEV Community

Sudhanshu code
Sudhanshu code

Posted on

From JavaScript to Node.js: Understanding What Really Happens Behind the Scenes (Part 4.2)

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
Enter fullscreen mode Exit fullscreen mode

});

Every parameter has a purpose.

  1. require const fs = require("fs");

require() loads another module.

Node creates this function before your code starts executing.

It is not provided by V8.

  1. module

Every file gets a module object.

Conceptually:

module = {

exports: {}
Enter fullscreen mode Exit fullscreen mode

}

Initially,

module.exports

is simply an empty object.

  1. 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.

  1. __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.

  1. __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
Enter fullscreen mode Exit fullscreen mode

}
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)