Welcome back to Jurassic Park, @name! Today we are looking at "hoisting" - a mechanism that decides what your code can reach and at which moment. Understanding hoisting will keep our park management systems predictable, because a variable that exists but holds nothing is far more dangerous than a variable that does not exist at all.
Hoisting is the JavaScript behavior of "lifting" variable and function declarations to the top of their scope (function or global) during the compilation phase, before a single line is executed. In other words, JavaScript picks up the declarations, moves them to the very top, and thanks to that we can refer to some variables and functions even before the place where they are actually declared in the code.
It works a bit like preparing the dinosaur inventory list before the animals are brought into the park - the JavaScript compiler first writes down "what to expect", and only then performs the detailed operations. Only the declarations travel to the top, never the values assigned to them.
Function declarations are hoisted completely, which means you can call them before they are defined in the code. That is genuinely useful in park management, where we like to keep the main logic at the top of a file and push the supporting details further down.
1// We can call the function before it is defined
2checkSecurity("T-Rex enclosure"); // "Checking security: T-Rex enclosure"
3
4// The function definition is hoisted
5function checkSecurity(location) {
6 console.log(`Checking security: ${location}`);
7 return true;
8}This works because during the compilation phase JavaScript lifts the entire function declaration - the name together with its body - to the top of the scope. By the time the first line runs,
checkSecurity is already a complete, callable function, not a placeholder.Only function declarations are hoisted in full. Function expressions, meaning functions assigned to a variable, follow the rules of variable hoisting: the variable itself is lifted, but the function assigned to it is not.
1// This will NOT work correctly
2// feedDinosaur("Rex"); // TypeError: feedDinosaur is not a function
3
4// Only the variable declaration is hoisted, not the function assignment
5var feedDinosaur = function(dinoName) {
6 console.log(`Feeding dinosaur: ${dinoName}`);
7};
8
9// From this line on we can call the function
10feedDinosaur("Rex"); // "Feeding dinosaur: Rex"Read that error message closely, because it is a useful clue. The words
feedDinosaur is not a function tell us that the name does exist - it is simply holding undefined at that moment, and undefined cannot be called. A function declaration is usable anywhere in its scope, a function expression only after the line that assigns it.Arrow functions are function expressions as well, so their body is never hoisted either. When you assign one to
const or let, the error you get is even stricter than the one produced by var.1// checkFence(); // ReferenceError: Cannot access 'checkFence' before initialization
2
3const checkFence = () => "Fence OK";
4
5console.log(checkFence()); // "Fence OK"Notice the difference: with
var we received a TypeError, here we receive a ReferenceError. Both mean "too early", but the const version stops us right at the declaration instead of letting an undefined value travel deeper into the program and break something far away from the real cause.For variables, hoisting behaves differently depending on the keyword used to declare them (
var, let or const). This is where most surprises come from, so let us take the two cases one at a time.Variables declared with
var are hoisted and automatically initialized with undefined, which means reading them early is legal:1console.log(dinosaurCount); // undefined (not an error!)
2var dinosaurCount = 15;
3console.log(dinosaurCount); // 15Nothing crashed here, and that is exactly the problem - the program keeps running with a value nobody intended. It helps to imagine that JavaScript rewrites the code above into the following shape before executing it:
1var dinosaurCount; // Hoisting - the declaration moves up
2console.log(dinosaurCount); // undefined
3dinosaurCount = 15; // The actual assignment stays where it was
4console.log(dinosaurCount); // 15The declaration climbed to the top, the assignment stayed exactly where you wrote it. Everything between those two points is a window in which the variable is reachable but empty, and a park sensor reporting
undefined looks alarmingly similar to a sensor reporting nothing at all.Variables declared with
let and const (introduced in ES6) are hoisted as well, but they are not initialized with undefined. Instead they stay in the "temporal dead zone" - the stretch of time from the moment the block starts executing until the line where the variable is actually declared.1// This will throw an error:
2// console.log(securityLevel); // ReferenceError: Cannot access 'securityLevel' before initialization
3let securityLevel = "High";
4console.log(securityLevel); // "High"
5
6// The same applies to const:
7// console.log(parkName); // ReferenceError: Cannot access 'parkName' before initialization
8const parkName = "Jurassic Park";
9console.log(parkName); // "Jurassic Park"It is worth learning to tell the two error messages apart:
ReferenceError: xyz is not definedlet or const variable inside the temporal dead zone: ReferenceError: Cannot access 'xyz' before initializationThat distinction is a debugging gift. The first case means the variable does not exist at all - most likely a typo or a missing import. The second means it exists, but you reached for it too early. Picture a function of two hundred lines where a value is read at the top and assigned at the bottom: with
var you silently get undefined and hunt the bug for an hour, with let the program stops immediately and points at the exact line. The temporal dead zone is not a limitation, it is an early warning system.Classes behave like
let and const rather than like function declarations. Their name is hoisted, but until the class body is evaluated any attempt to use it ends in an error.1// const dino = new Dinosaur("Rex"); // ReferenceError: Cannot access 'Dinosaur' before initialization
2
3class Dinosaur {
4 constructor(name) {
5 this.name = name;
6 }
7}
8
9const dino = new Dinosaur("Rex"); // OKSo the rule "functions can be defined below their usage" does not extend to classes - a class has to be defined before the first
new that creates an instance of it. If you ever move a class to the bottom of a file out of habit, this is the error you will meet.Hoisting behavior is tightly connected with variable scope. Declarations are only lifted to the top of their current scope, and which scope that is depends on the keyword:
var uses function scope, while let and const use block scope.1function monitorSystem() {
2 console.log(status); // undefined (hoisted within the function)
3
4 if (true) {
5 var status = "Online";
6 }
7
8 console.log(status); // "Online"
9}
10
11monitorSystem();
12// console.log(status); // ReferenceError: status is not defined (outside the function scope)The
if block did not create a new scope for status at all. The declaration jumped to the top of monitorSystem, so the variable is readable before the block, holds undefined there, and keeps its value after the block. Outside the function it disappears completely, because function scope is the only boundary var respects.1function checkEnclosures() {
2 // console.log(enclosureStatus); // ReferenceError (temporal dead zone)
3
4 if (true) {
5 let enclosureStatus = "Secure";
6 console.log(enclosureStatus); // "Secure"
7 }
8
9 // console.log(enclosureStatus); // ReferenceError (outside the block scope)
10}
11
12checkEnclosures();Here the curly braces really are a wall. The variable exists only between them, and both before and after the block the same
ReferenceError appears. The same rule applies to loops, which is worth remembering before you experiment on your own: a counter declared as for (let i = 0; ...) lives only inside the loop, while a counter declared as for (var j = 0; ...) is hoisted to the top of the function and is still visible - with its final value - long after the loop has finished.Function hoisting lets us organize a file by importance instead of by execution order. The main routine goes first, the helpers it relies on go at the bottom, and everything still works.
1// Dinosaur monitoring system
2
3// The main functions are defined at the top for clarity
4function initMonitoringSystem() {
5 console.log("Initializing the dinosaur monitoring system...");
6
7 // We can call these functions even though they are defined later
8 const healthStatus = checkDinosaurHealth("Rex");
9 const locationData = trackDinosaur("Blue");
10
11 return {
12 health: healthStatus,
13 location: locationData,
14 systemStatus: "Online"
15 };
16}
17
18// Calling the main function
19const systemStatus = initMonitoringSystem();
20console.log(systemStatus);
21
22// Helper functions defined at the bottom of the file
23function checkDinosaurHealth(dinoName) {
24 console.log(`Checking dinosaur health: ${dinoName}`);
25 return "Healthy";
26}
27
28function trackDinosaur(dinoName) {
29 console.log(`Tracking dinosaur: ${dinoName}`);
30 return { x: 135, y: 270, area: "Sector B" };
31}Notice that
initMonitoringSystem is called on a line where neither helper has been reached yet, and the code still runs. That is pure function hoisting - both declarations were complete before the first statement executed.The same mechanism can work against us. Watch what happens when an inner function declares a variable that already exists in the outer scope.
1// A trap connected with hoisting
2
3// A function that checks the security systems
4function checkSecurity() {
5 var fenceStatus = "Offline"; // Hoisted to the top of the function
6
7 function enableFences() {
8 // This variable is hoisted to the top of enableFences,
9 // but it does not hold a value until the initialization line
10 console.log("Current fence status before the change:", fenceStatus); // undefined!
11
12 // The local fenceStatus shadows the one from the outer scope
13 var fenceStatus = "Online";
14 console.log("New fence status:", fenceStatus); // "Online"
15 }
16
17 enableFences();
18 console.log("Fence status in the main function:", fenceStatus); // "Offline" (unchanged)
19}
20
21checkSecurity();There is a subtle trap in the example above - the
console.log inside enableFences prints undefined, because the local fenceStatus is hoisted but not yet initialized, and from the very first line of the function it already shadows the variable from the outer scope. The outer fenceStatus never changes either, so the fences stay offline while the log cheerfully reports "Online".Swapping
var for let and const removes the whole class of problems shown above, because reading a value too early stops the program instead of quietly producing undefined.1// A better approach using let and const
2
3function parkOperations() {
4 // We use const for values that should not change
5 const maxVisitors = 2000;
6
7 // We use let for variables that will change
8 let currentVisitors = 0;
9
10 function admitVisitors(count) {
11 // With let and const, reading a variable before its declaration throws an error,
12 // which makes potential problems much easier to spot
13
14 // This would throw an error:
15 // console.log(availableSpace); // ReferenceError: Cannot access before initialization
16
17 // First we declare, then we use
18 const availableSpace = maxVisitors - currentVisitors;
19 if (count <= availableSpace) {
20 currentVisitors += count;
21 console.log(`Admitted ${count} visitors. Currently in the park: ${currentVisitors}`);
22 return true;
23 }
24
25 console.log(`Too many visitors! Available spots: ${availableSpace}`);
26 return false;
27 }
28
29 // Testing our function
30 admitVisitors(500); // Admitted 500 visitors
31 admitVisitors(1000); // Admitted 1000 visitors
32 admitVisitors(700); // Too many visitors! Available spots: 500
33
34 return currentVisitors;
35}
36
37console.log(`Total number of visitors: ${parkOperations()}`);Every value here is declared exactly where it is needed and never before it can be computed, so the gate logic can be read from top to bottom without holding hoisting rules in your head. That is the practical goal - not memorising the mechanism, but writing code where the mechanism never surprises you.
Always declare variables at the top of their scope - this minimizes hoisting-related problems and makes the code easier to read.
Prefer
and let
over const
- temporal dead zone errors are far easier to diagnose than mysterious var
undefined values.Declare functions before you use them - function hoisting works, but declaring first still improves readability.
Avoid function declarations inside conditional blocks - the behavior can differ between browsers.
Remember that hoisting applies to declarations, not to initializations - the assignment happens exactly where you wrote it.
Below is a more complete incident management system for Jurassic Park, which puts together everything from this lesson: hoisted function declarations, a
var variable at module level, and let and const used for state that changes and state that must not.1// Jurassic Park incident management system
2
3// Taking advantage of hoisting - we declare the variables at the top
4var activeIncidents = [];
5let alertLevel = "Normal";
6const MAX_INCIDENTS = 5;
7
8// The main function that initializes the system
9function initIncidentSystem() {
10 // This variable is hoisted only within this function
11 var systemStatus = "Initializing";
12
13 // We use function hoisting - we can call these before their definition
14 logSystemStatus();
15 resetIncidents();
16
17 systemStatus = "Online";
18 logSystemStatus();
19
20 // A local function - visible only inside initIncidentSystem
21 function logSystemStatus() {
22 console.log(`System status: ${systemStatus}`);
23 }
24
25 return {
26 reportIncident, // A reference to a function defined below
27 checkAlertLevel, // A function defined below
28 getActiveIncidents, // A function defined below
29 resetIncidents // A function defined inside initIncidentSystem
30 };
31
32 // A local function that we return as part of the API
33 function resetIncidents() {
34 console.log("Resetting the incident list...");
35 activeIncidents = [];
36 updateAlertLevel();
37 }
38}
39
40// Function declarations are hoisted, so they can be called before their definition
41// Reporting a new incident
42function reportIncident(location, type, severity) {
43 console.log(`Incident reported: ${type} at ${location} (severity: ${severity})`);
44
45 const incident = {
46 id: generateIncidentId(),
47 location,
48 type,
49 severity,
50 timestamp: new Date().toISOString(),
51 status: "Active"
52 };
53
54 activeIncidents.push(incident);
55
56 if (activeIncidents.length > MAX_INCIDENTS) {
57 declareEmergency();
58 } else {
59 updateAlertLevel();
60 }
61
62 return incident;
63}
64
65// Generating a unique incident id
66function generateIncidentId() {
67 return `INC-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
68}
69
70// Updating the alert level based on the active incidents
71function updateAlertLevel() {
72 const highSeverityCount = activeIncidents.filter(inc => inc.severity === "High").length;
73
74 if (highSeverityCount >= 3) {
75 alertLevel = "Critical";
76 } else if (highSeverityCount > 0 || activeIncidents.length >= 3) {
77 alertLevel = "Elevated";
78 } else if (activeIncidents.length > 0) {
79 alertLevel = "Caution";
80 } else {
81 alertLevel = "Normal";
82 }
83
84 console.log(`Alert level updated: ${alertLevel}`);
85}
86
87// Called when there are too many incidents at once
88function declareEmergency() {
89 alertLevel = "Evacuation";
90 console.log("WARNING! Too many active incidents. Park evacuation declared!");
91 // Code that starts the evacuation protocols...
92}
93
94// Checking the current alert level
95function checkAlertLevel() {
96 return {
97 level: alertLevel,
98 incidentCount: activeIncidents.length,
99 timestamp: new Date().toISOString()
100 };
101}
102
103// Returning the list of active incidents
104function getActiveIncidents() {
105 return [...activeIncidents]; // A copy, so nobody modifies the original directly
106}
107
108// Initializing the system
109const incidentSystem = initIncidentSystem();
110
111// Testing the system
112console.log("===== TESTING THE INCIDENT MANAGEMENT SYSTEM =====");
113
114// Checking the initial state
115console.log("Initial alert level:", incidentSystem.checkAlertLevel().level);
116
117// Reporting incidents
118incidentSystem.reportIncident("T-Rex Enclosure", "Fence damage", "High");
119incidentSystem.reportIncident("Visitor Center", "Power outage", "Medium");
120
121// Checking the state after the reports
122console.log("Active incidents:", incidentSystem.getActiveIncidents().length);
123console.log("Current alert level:", incidentSystem.checkAlertLevel().level);
124
125// Adding more high priority incidents
126incidentSystem.reportIncident("Laboratory", "Genetic material leak", "High");
127incidentSystem.reportIncident("Velociraptor Enclosure", "Escape attempt", "High");
128
129// This should trigger the evacuation state
130incidentSystem.reportIncident("Sector B", "Dinosaur outside the enclosure", "High");
131incidentSystem.reportIncident("Operations Center", "Security system failure", "High");
132
133// Resetting the system
134incidentSystem.resetIncidents();
135console.log("After reset - alert level:", incidentSystem.checkAlertLevel().level);Trace the order in which things happen here:
initIncidentSystem calls logSystemStatus and resetIncidents before either appears in the source, and resetIncidents even sits after the return statement - yet all of it works, because function declarations are complete before the first line of the body executes. The var systemStatus inside the function is a different story: it is readable from the start, but it only becomes "Initializing" on the line that assigns it.Hoisting is an important JavaScript concept that shapes how our code executes:
Function declarations are hoisted in full - you can use them before their declaration.
var variables are hoisted and initialized with
undefined - you can reference them early, but until the assignment they carry no useful value.let and const variables are hoisted but stay in the temporal dead zone - reading them before the declaration throws an error.
Function expressions (including arrow functions) follow the variable rules - the variable is hoisted, the assigned function is not.
Understanding hoisting is essential for writing predictable JavaScript. It works just like Jurassic Park itself - knowing the rules and the protocols is what keeps surprises, and dangerous situations, out of the day.
"Hoisting in JavaScript is like the morning park setup" - says Dr. Rex. "Before the visitors arrive, before the code runs, certain preparations happen automatically: the protocols are ready, the procedures are in place. But the actual data still has to be gathered in real time, and reaching for it too early tells you nothing useful!"
In the next lesson we will take on the
this keyword, another fundamental and occasionally confusing part of JavaScript.