Skip to content

JavaScript Essentials

Node.js is a runtime that executes JavaScript outside the browser. Everything you write in Node — from a simple HTTP server to a complex CLI tool — is JavaScript. That means a solid grip on the language itself is non-negotiable before diving into Node’s APIs.

This module covers the JavaScript fundamentals that trip up developers most in the Node ecosystem:

LessonTopic
Types and CoercionPrimitives vs objects, typeof, == vs ===, truthy/falsy
Scope and Closuresvar/let/const, block scope, hoisting, lexical scope
this and ContextDynamic this, call/apply/bind, arrow functions
ModulesESM import/export vs CommonJS require, when to use each

Modern JavaScript means ES2022 and later throughout — const/let, arrow functions, destructuring, async/await, optional chaining, and native ES modules. This module targets Node 24 (the active LTS); everything also runs on Node 26.

The snippet below demonstrates several JS concepts at once: a const declaration, a function, a template-string-free concatenation, and a closure over the outer variable. Run it and inspect the output.

const greet = function(name) {
return 'Hello, ' + name + '!';
};
const languages = ['JavaScript', 'TypeScript', 'Node.js'];
for (const lang of languages) {
console.log(greet(lang));
}
JavaScript

Node upgrades V8 with every major release, so the language keeps growing:

  • Node 24 — using / await using for automatic resource cleanup, RegExp.escape(), Error.isError(), Float16Array.
  • Node 26 — the Temporal date/time API is enabled by default, plus Map.prototype.getOrInsert() and Iterator.concat().
// @skip-verify Node 26-only (Temporal) + illustrative openConnection()
// Node 26: Temporal replaces error-prone Date arithmetic
const due = Temporal.Now.plainDateISO().add({ days: 30 });
console.log(due.toString()); // e.g. 2026-10-12
// Node 24: using — the resource is disposed when the block exits
{
using conn = openConnection(); // the object has a [Symbol.dispose]() method
conn.query('SELECT 1');
} // conn[Symbol.dispose]() runs here, even on throw

Every piece of JavaScript runs in one of two modes, decided by how the file is loaded, not by what it does:

  • Sloppy mode — the historical default. A classic <script> tag in a browser, or a .js file in a package without "type": "module", runs sloppy unless it opts out. Sloppy mode forgives mistakes that would otherwise be bugs: assigning to a name that was never declared silently creates a global variable instead of throwing, and a plain function call sets this to the global object instead of undefined.
  • Strict mode — turns those silent forgivenesses into thrown errors. You get strict mode in three ways: writing 'use strict'; as the very first statement in a script or function, loading the file as an ES module (.mjs, or .js with "type": "module" — always strict, no directive needed), or writing code inside a class body (class bodies are strict even in an otherwise sloppy file).
flowchart TD
  A["How is this code loaded?"] --> B["Classic script / CommonJS .js file"]
  A --> C["ES module: .mjs, or .js with package.json type: module"]
  A --> D["Any code inside a class body"]
  B -->|"no directive"| E["Sloppy mode"]
  B -->|"'use strict' as first line"| F["Strict mode"]
  C --> F
  D --> F
  E --> G["Undeclared assignment creates a global; this === global object in a plain call"]
  F --> H["Undeclared assignment throws ReferenceError; this === undefined in a plain call"]
What decides sloppy vs strict mode

This is exactly why the this and Context demo later in this module opens with 'use strict';: without it, a detached method call would silently fail by mutating the wrong object instead of throwing an error you can see and fix.

// @skip-verify illustrative — contrasts what a plain (sloppy) script allows
// vs what strict mode (any ES module, any class, or an explicit directive) rejects
function setTotal() {
total = 42; // no const/let/var
}
setTotal();
console.log(total); // sloppy mode: 42 — an implicit global was created
JavaScript

Think of sloppy mode as JavaScript running with a permissive safety net: typos and missing declarations get quietly absorbed instead of stopping the program. Strict mode removes that net. It does not add new features — it removes silent failure paths, converting them into thrown errors at the exact line where the mistake happened. Because ES modules and classes are strict unconditionally, and essentially all new Node.js code is written as ES modules, you should default to assuming strict mode and treat sloppy mode as the legacy exception you occasionally still meet in old CommonJS files.

ModeBenefitCost
Sloppy (legacy default)Old scripts keep running unmodified; nothing new to learnTypos become invisible global variables instead of errors; this in a plain call silently becomes the global object
Strict (ESM, classes, 'use strict')Mistakes surface immediately as ReferenceError/TypeError at the point of the bugA handful of legacy patterns (some arguments tricks, with) stop working — rarely missed in modern code
  • “'use strict' is an old ES5 relic nobody needs anymore.” You still need it in plain CommonJS .js/.cjs files if you want strict guarantees there — but you get it for free, with no directive, in every ES module and every class.
  • “Strict mode changes what the code computes.” It does not add or change arithmetic or object semantics; it only turns previously-silent mistakes into thrown errors.
  • “If my file has "type": "module" in package.json, I still need 'use strict' at the top.” No — the directive is redundant there; ESM is strict unconditionally.

AI-generated snippets are frequently written and tested as quick CommonJS scripts (sloppy by default) and then assumed to behave identically once dropped into a class or an ES module. Watch for code that relies on an assignment “just working” without a preceding const/let/var — that is a latent bug that only a strict context (ESM, a class, or an explicit directive) will actually catch for you.

🐛 Spot the bug

function cache(key, value) {
if (!store) store = {}; // no declaration — meant to be a shared cache
store[key] = value;
return store;
}
cache('a', 1);
cache('b', 2);
console.log(store); // "works" by accident today
Show the answer

store is never declared with const/let/var. In a sloppy CommonJS script this silently creates a global named store the first time cache runs, which is why it “works” — every call happens to share the same accidental global. Drop this same function into an ES module, a class method, or add 'use strict' to the file, and the first call throws ReferenceError: store is not defined instead. The fix is to declare the variable in an enclosing scope (module-level const store = {};) so the behavior does not depend on which mode the file happens to run in.

What does Node.js provide that a browser does not?
Which JavaScript version features does this module rely on?
Which of these three contexts is strict mode **not** automatic in, and needs an explicit 'use strict'; directive to get it?
What does this print when run as a plain, sloppy CommonJS script (no 'use strict')?
function setTotal() {
  total = 42;
}
setTotal();
console.log(total);
Why does this function silently misbehave if it is ever moved into an ES module?
function cache(key, value) {
  if (!store) store = {};
  store[key] = value;
  return store;
}
cache('a', 1);