protips

Logo

This site lists the protips that we shared with students during our courses

View the Project on GitHub appliedtechnology/protips

What does require really do - and something about how JavaScript works as well

This blog post came about after a discussion with a seasoned developer that took our entire course, worked as an instructor, and still went:

WHA?! I had no idea that it worked like that

To the defense of the instructor, I want to say that this is very typical for how you learn and is well-documented through the Dunning-Kruger. When we learn a topic we have to abstract some of the underlying layers away to make sense of it all. Once we see this deeper layer of how things work, we often realize how little we work.

So no shame should fall on the instructor, but rather congratulations as a new level of understanding were reached!

Ok - so what was the curtain that was opened for this poor, unnamed instructor?

What does require do?

When we want to use a function from one file in another we use require to import it into our file. Like this:

// in include.js
const add = (num1, num2) => num1 + num2;

module.exports = {
  add
}

// in index.js
const adder = require('./include');

console.log(adder.add(1,3))

If you create these two files in a directory and run node index.js (or node .) we expect, and get: 4 printed to the console.

But what happens if we run node include.js? Think about it before you read on.

No - you didn’t think. You just read on. Think.

Ok - here we go. Nothing happens. Because include.js doesn’t do anything. It just declares a function add that is exported and never used. Let’s add a console.log and test it out:

const add = (num1, num2) => num1 + num2;
console.log(add(3, 4));

module.exports = {
  add,
};

Now that we run node include.js the add function is used and hence a glorious 7 is printed.

Let’s rerun node . and see what happens. What do you think?

Well, this might surprise you but you get both 7 and 4 printed. Why is that?

What does require do?

From the documentation we learn that:

The basic functionality of require is that it reads a JavaScript file, executes the file, and then proceeds to return the exports object.

The code gets executed from top to bottom, and then any exported functions are returned and stored in whatever variable you might have.

This might first surprise you but is not so surprising once you think that JavaScript is an interpreting scripting language. To Node, for example, we pass a file with node apa.js, node will execute the file, line by line starting from the top.

So a file with only function declarations (like our original include.js, before the console.log(add(3, 4));) just has a bunch (eehh one…) of function declarations. Since no one is calling that function nothing happens. Adding the console.log(add(3, 4)); will call add and hence 7 is printed.

If we then proceed to include include.js using const adder = require('./include'); the same thing happen:

  1. require('./include'); will “reads a JavaScript file, executes the file, and then .. return the exports object”
  2. Since the include.js file holds a console.log-statement it will get executed and 7 is printed to the console.
  3. When the whole included.js is processed the module.exports is returned and stored in the adder constant.

What about if I’m using import/export?

The same thing happens for import/export because it’s really all JavaScript can do. It can just execute line by line in files. Let’s test it out:

// in include2.mjs
export const add = (num1, num2) => num1 + num2;

console.log(add(3,4))


// in index2.mjs
import { add } from './include2.mjs';

console.log(add(1, 3));

(Note the file extension .mjs which stands for Module JavaScript that is the modern way of writing JavaScript code. However, Node used the CommonJS loader (via require) by default, but we can force Node to use ECMAScript modules by using the .mjs extension. This is wicked cool since that means that we can use async/await and all those cool things in Node script, but that is beside the point.)

Ok - back on track… with those two files we can now go node index2.mjs and see that we get the 7 and 4 printed again. Because it works the same way; the file is read, executed line by line and then the exports are exported.

That is cute, but these are just kindergarten examples

Ok - let’s see how this works in action, in an Express API for example:

// in index.js
const express = require('express')
const app = express()
const port = 3000

const getRootHandler = (req, res) => {
  res.send('Hello World!')
}

app.get('/', getRootHandler)

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

console.log('OOOI, being executed over here');

That is the Hello World version of Express (with a small addition by me :), but what happens when you start this with node index.js (you will need to create a node application too mkdir demo && cd demo && npm init -y):

  1. node index.js will read the file
  2. It will execute the file line by line
  3. require('express') will read that package and execute the files included
  4. Express() executes the main function of the Express package.
  5. We then declare a function getRootHandler.
  6. We then proceed to register a few functions as callbacks to the. So nothing is executed since no one has made a request to / yet.
  7. We then declare a callback function to the app.listen function.
  8. The final line is a little logging of my own, and that is printed
  9. When the entire file is executed line by line, express does its thing, first which is (probably) to execute whatever callback is attached to app.listen.

Hence is printed:

OOOI, being executed over here
Example app listening at http://localhost:3000

Summary and read more

I hope you found this enlightening and useful. Should you have found it confusing I’m sure this article will do a better job than me explaining it.