Skip to main content
CodeOath
← All posts

JavaScript140 min total · 15 parts

JavaScript Core Concepts: Scope, Closures, `this`, and the Event Loop

Part 4 of 15 · ~3 min

Hoisting and the Temporal Dead Zone

Our search box file has grown past twelve lines, so let us organise it the way you would organise any real file: the entry point at the top where a reader will find it, the helpers below it.

setupSearch();

function setupSearch() {
  const input = document.querySelector("#search-input");
  input.addEventListener("input", () => renderResults(search(input.value)));
}

function search(query) {
  return ARTICLES.filter((a) => a.title.includes(normalize(query)));
}

function normalize(query) {
  return query.trim().toLowerCase();
}

Read that top to bottom and it looks like it cannot possibly work. We call setupSearch() on line one, and setupSearch is not defined until line three. search calls normalize before normalize exists. And yet it runs perfectly.

Hoisting is the reason: declarations are processed before any code in their scope actually runs. Before the first line of your file executes, JavaScript has already walked the whole scope and made a note of every name declared in it. By the time line one runs, setupSearch is not a promise of a future function — it is already a complete, callable function, body and all.

That is the intuitive version, and it is enough to explain the file above. Now the precise version, because what gets hoisted differs sharply by declaration type, and the differences are where the errors live.

console.log(cachedResults); // undefined — hoisted AND initialized to undefined
var cachedResults = new Map();

console.log(latestQuery); // ReferenceError: Cannot access 'latestQuery' before initialization
let latestQuery = "";

console.log(normalize("  Closures ")); // "closures" — function declarations hoist completely, body included
function normalize(query) { return query.trim().toLowerCase(); }

console.log(formatTitle("Closures")); // ReferenceError: Cannot access 'formatTitle' before initialization
const formatTitle = (t) => t.toUpperCase();

Four declarations, four genuinely different behaviours:

  • var is hoisted and immediately initialized to undefined. Read it before its assignment line and you get undefined — no error, no warning, just a value that is probably wrong. cachedResults above is not a Map yet; it is undefined, and the .get() call you were about to write will fail three lines later with a message that points nowhere near the real problem.
  • let and const are hoisted too — the name is reserved in the scope — but they are left uninitialized. The stretch between the top of the scope and the line that actually declares them is the temporal dead zone, and touching the name anywhere inside it throws a ReferenceError. This is a feature. var handing you undefined is strictly worse than an error that names the problem.
  • Function declarations (function normalize() {}) hoist completely, body and all. This is what lets you put your entry point above your helpers, and it is worth using deliberately — reading order and execution order do not have to match.
  • Arrow functions and function expressions assigned to let/const hoist as variables, so they land in the TDZ like any other const. The function itself is fine; the name holding it is not ready yet.

That last one deserves a beat, because it is the one that actually catches people, and it catches them during refactors rather than while writing fresh code. Our file works today. Now suppose you tidy it — pull formatTitle out as a const arrow function, drop it at the bottom next to the other helpers, and move the call to setupSearch() up top where it is easy to find. Function declarations will follow you anywhere in the file. const arrow functions will not. The code that broke is code you did not touch.

There is one more distinction hiding in that list, and it is worth naming explicitly because it changes the error message you get. var gives you a variable that exists but holds undefined, so calling it produces a TypeError — undefined is not a function. let and const give you a name you are not allowed to touch yet, so reaching for it produces a ReferenceError before any call is even attempted. Two different failures, two different messages, and knowing which is which tells you immediately whether you are looking at a TDZ problem or a var problem.