JavaScript and TypeScript course Β· Module 2: Functions, Arrays and Objects
Error Handling in JavaScript
In this lesson7
In Jurassic Park even the best planned systems fail - a dinosaur breaks out of its enclosure, a sensor stops reporting, an automated feeder receives corrupted data. That is exactly why every park safety system needs emergency protocols. In JavaScript that protocol is the try/catch/finally mechanism.
The try/catch block - the safety protocol
Imagine that every operation in the park is a potential hazard. The try block is the zone where we run the risky operation, and catch is the rescue team that steps in when something goes wrong.
1try {
2 // The risky zone - an attempt to open the enclosure
3 const dinoData = JSON.parse('{ broken JSON }');
4 console.log(dinoData.species);
5} catch (error) {
6 // The rescue team - handling the error
7 console.log("ALARM! System failure:", error.message);
8}
9console.log("The system keeps running - the park was not shut down!");Without try/catch our program would stop dead on that error - as if the whole park had to be evacuated because of one faulty sensor. Thanks to error handling, execution continues once the problem has been dealt with, which is why the last line still prints.
Pay attention to how the error actually reaches us. JSON.parse does not return an error value you could compare with ===, and it has no .onError() callback to register either - a thrown error can only be intercepted by a catch block. That is why the only correct way to parse untrusted data is try { JSON.parse(data); } catch (error) { handleError(error); }.
The anatomy never changes: the keyword try, then a block in braces holding the risky code, then the keyword catch, then the error parameter in parentheses, and finally the block that handles the problem. Learn that five-part shape once and you can write it from memory.
The finally block - the closing protocol
finally is a block that always executes - regardless of whether an error occurred or not. It is the fence lock-down procedure that has to happen whatever the outcome of the mission.
1function feedDinosaur(dinoName, food) {
2 console.log("Opening the feeder for " + dinoName + "...");
3
4 try {
5 if (!food) {
6 throw new Error("No food left in the system!");
7 }
8 console.log("Serving " + food + " to " + dinoName);
9 } catch (error) {
10 console.log("ALARM: " + error.message);
11 } finally {
12 // ALWAYS close the feeder - whether the feeding worked or not
13 console.log("Closing the feeder - safety first!");
14 }
15}
16
17feedDinosaur("Rex", "meat"); // Success + closing
18feedDinosaur("Rex", null); // Error + closing (finally always runs!)Compare the two calls. The first feeding succeeds, so catch never runs - and the feeder is still closed. The second one throws, catch reports the alarm - and the feeder is still closed. This is the detail people get wrong most often: finally does not fire only when an error occurs, nor only when no error occurs, nor only when catch handled the error successfully. It fires every single time.
Put together, a full handling cycle goes through five stages:
- Code in the
tryblock encounters an error - Execution passes to the
catchblock with the error object - The
catchblock handles the error (logging, recovery) - The
finallyblock performs cleanup operations (resource cleanup) - The program continues execution after the try/catch/finally block
So the fourth stage is where we put everything that has to be released no matter how the mission went: connections, files and feeders that someone left open.
Error classes in JavaScript
JavaScript ships with a built-in hierarchy of errors, much like the park has different alert levels for different kinds of threat.
The main error types
- Error - a generic failure (a general park alert)
- TypeError - an operation on the wrong data type (feeding meat to a herbivore)
- RangeError - a value outside the allowed range (incubator temperature off the scale)
- ReferenceError - a reference to a variable that does not exist (a dinosaur missing from the database)
- SyntaxError - invalid code syntax (a corrupted DNA blueprint)
Every one of these classes can be recognised inside catch using instanceof, and that is how you decide which emergency procedure to follow:
1// TypeError - an operation on the wrong type
2try {
3 const dino = null;
4 dino.roar(); // null has no methods!
5} catch (error) {
6 console.log(error instanceof TypeError); // true
7 console.log(error.message); // "Cannot read properties of null"
8}
9
10// RangeError - a value outside the allowed range
11try {
12 const dinoArray = new Array(-5); // Negative array length!
13} catch (error) {
14 console.log(error instanceof RangeError); // true
15}
16
17// ReferenceError - a variable that does not exist
18try {
19 console.log(missingDinosaur);
20} catch (error) {
21 console.log(error instanceof ReferenceError); // true
22}Three different failures, three different classes, but the same catch shape handles them all. The message property carries the description a human can read, while instanceof gives you the category a program can branch on.
SyntaxError is the odd one out, because broken syntax in your own file is usually caught before the program even starts. You still meet it at runtime in two places: inside eval, and - far more often - when parsing malformed JSON.
1// SyntaxError - broken code, normally caught before the program runs
2try {
3 eval("function(");
4} catch (error) {
5 console.log(error instanceof SyntaxError); // true
6}
7
8// The same class comes back from corrupted JSON
9try {
10 JSON.parse("{ broken JSON }");
11} catch (error) {
12 console.log(error instanceof SyntaxError); // true
13}That second example closes the loop on our very first snippet: the alarm raised by JSON.parse is a SyntaxError, so a sensor feed that arrives damaged can be told apart from a sensor feed that arrives complete but wrong.
Throw - raising the alarm
The throw keyword lets us raise an error on purpose - like pulling the manual alarm in the park the moment we spot a problem, before the automatic systems get around to detecting it.
1function checkEnclosureFence(voltage) {
2 if (voltage < 0) {
3 throw new RangeError("Voltage cannot be negative: " + voltage);
4 }
5 if (voltage < 5000) {
6 throw new Error("ALARM: Fence voltage below minimum! (" + voltage + "V)");
7 }
8 console.log("Fence operational: " + voltage + "V");
9}
10
11try {
12 checkEnclosureFence(10000); // OK
13 checkEnclosureFence(3000); // Throws an Error
14} catch (error) {
15 console.log("Problem detected:", error.message);
16}Two guards, two different classes: a negative reading is physically impossible, so it is a RangeError, while a fence that is merely too weak is a plain Error. The moment throw fires, the rest of the function is abandoned - the second call never prints "Fence operational", control jumps straight into catch, and the third statement in the try block would never run either. Use the same technique for lookups: when a search finds no matching dinosaur, throwing an Error is far safer than quietly returning undefined.
Re-throw - passing the alarm up the chain
Sometimes catch intercepts an error it cannot actually deal with. In that case we throw it again (a re-throw), so that somebody higher up the chain of command can react.
1function processEnclosureData(data) {
2 try {
3 const parsed = JSON.parse(data);
4 if (!parsed.species) {
5 throw new Error("No species in the enclosure data");
6 }
7 return parsed;
8 } catch (error) {
9 if (error instanceof SyntaxError) {
10 // We know how to handle this one - the payload is corrupted
11 console.log("Corrupted data, falling back to default values");
12 return { species: "Unknown", status: "error" };
13 }
14 // Anything else - hand it over to the caller
15 throw error;
16 }
17}This function is deliberately picky. Corrupted JSON produces a SyntaxError, which it recovers from with a safe default record. A missing species is a different problem entirely - the data parsed fine, so silently guessing would hide a real fault in the park database, and the error travels upward instead. Swallowing every error in a bare catch is how bugs stay invisible for months.
Custom error classes
In Jurassic Park we need specialised emergency procedures for different situations. In JavaScript we get them by writing custom error classes that extend the built-in Error class.
1class DinosaurEscapeError extends Error {
2 constructor(species, enclosure) {
3 super("ALARM: " + species + " escaped from enclosure " + enclosure + "!");
4 this.name = "DinosaurEscapeError";
5 this.species = species;
6 this.enclosure = enclosure;
7 }
8}
9
10class FeedingError extends Error {
11 constructor(dinoName, reason) {
12 super("Feeding error for " + dinoName + ": " + reason);
13 this.name = "FeedingError";
14 this.dinoName = dinoName;
15 }
16}
17
18class EnclosureVoltageError extends Error {
19 constructor(enclosure, voltage) {
20 super("Voltage in enclosure " + enclosure + ": " + voltage + "V (minimum 5000V)");
21 this.name = "EnclosureVoltageError";
22 this.enclosure = enclosure;
23 this.voltage = voltage;
24 }
25}Each constructor passes the finished message to super(...), sets a readable name, and then stores the extra facts the responder will need - the species, the enclosure, the voltage reading. The same recipe gives you a SensorError for a probe that stops answering, and nothing stops you from putting a shared base class such as ParkError extends Error above them all, so that a single instanceof ParkError check can catch the whole family at once.
With the classes in place, the security sweep can react to each threat differently:
1// Usage with a selective catch
2function parkSecurityCheck(enclosures) {
3 for (const enc of enclosures) {
4 try {
5 if (enc.voltage < 5000) {
6 throw new EnclosureVoltageError(enc.name, enc.voltage);
7 }
8 if (enc.dinoEscaped) {
9 throw new DinosaurEscapeError(enc.species, enc.name);
10 }
11 console.log("Enclosure " + enc.name + " - status OK");
12 } catch (error) {
13 if (error instanceof DinosaurEscapeError) {
14 console.log("RED ALERT: " + error.message);
15 console.log("Species: " + error.species);
16 } else if (error instanceof EnclosureVoltageError) {
17 console.log("YELLOW ALERT: " + error.message);
18 } else {
19 throw error; // Unknown error - re-throw
20 }
21 }
22 }
23}Read the catch block as a triage desk: an escape gets a red alert with the species pulled straight off the error object, a weak fence gets a yellow alert, and anything unrecognised is re-thrown rather than guessed at. Because the loop sits outside nothing, one failing enclosure does not stop the sweep of the remaining ones. Custom error classes are what make this precise handling possible - different threats deserve different procedures, exactly as different alarms trigger different protocols in a real park.
Optional chaining (?.) and Nullish coalescing (??)
These two operators are the park's smart sensors - they let you move safely through data that may be incomplete, instead of crashing the control room the first time a field is missing.
Optional chaining (?.)
The ?. operator reaches into nested properties safely. If any value along the path is null or undefined, the whole expression stops there and gives back undefined instead of throwing.
1const dinoRecord = {
2 species: "Velociraptor",
3 tracker: {
4 lastLocation: "Sector C",
5 gps: { lat: 23.5, lng: -80.2 }
6 }
7};
8
9// Without optional chaining - risk of a TypeError
10// const lat = dinoRecord.medical.lastCheckup.date; // TypeError!
11
12// With optional chaining - safe access
13const lat = dinoRecord.medical?.lastCheckup?.date; // undefined (no error!)
14const gpsLat = dinoRecord.tracker?.gps?.lat; // 23.5
15
16// It works with method calls and indexes too
17const dinoCall = dinoRecord.sounds?.roar?.(); // undefined (no such method)
18const firstTag = dinoRecord.tags?.[0]; // undefined (no array here)Look closely at dinoRecord.medical?.lastCheckup?.date. Our record has no medical property at all, yet the line does not throw a TypeError - the chain simply stops at the first missing link and the expression returns undefined. Not null, not an empty string, not an exception: undefined. The commented-out line above shows what the same read costs you without ?..
Nullish coalescing (??)
The ?? operator supplies a default value when the left-hand side is null or undefined. Unlike ||, it does not react to 0, "" or false.
1// The problem with || - it treats 0 and "" as empty
2const dinoWeight = 0;
3console.log(dinoWeight || "No data"); // "No data" (wrong! 0 is a valid weight)
4console.log(dinoWeight ?? "No data"); // 0 (correct!)
5
6// A practical example with dinosaur records
7function getDinoInfo(dino) {
8 const name = dino?.name ?? "Unknown dinosaur";
9 const weight = dino?.weight ?? "no data";
10 const isHealthy = dino?.isHealthy ?? false;
11 const speed = dino?.speed ?? 0;
12
13 return name + " | Weight [kg]: " + weight + " | Healthy: " + isHealthy + " | Speed: " + speed;
14}
15
16console.log(getDinoInfo({ name: "Rex", weight: 8000 }));
17console.log(getDinoInfo(null)); // Safe - it will not throwThe first pair of lines is the whole argument in miniature: a newborn dinosaur really can weigh in at a measured 0, and || would throw that measurement away. Notice also that getDinoInfo(null) survives - dino?.name short-circuits before the property read, so ?. and ?? together turn a crash into a sensible fallback report.
The same protection matters for sensor configuration, where an explicit switched-off value is meaningful data rather than a gap to be filled.
1const sensorConfig = {
2 voltage: 0, // deliberately zero
3 active: false, // deliberately off
4 label: "" // deliberately blank
5};
6
7console.log(sensorConfig.voltage ?? 10000); // 0 (the explicit 0 survives)
8console.log(sensorConfig.active ?? true); // false (the explicit false survives)
9console.log(sensorConfig.label ?? "unnamed"); // "" (the explicit empty string survives)
10console.log(sensorConfig.missing ?? "N/A"); // "N/A" (undefined, so the default wins)Only the last line falls back, because only the last property is genuinely absent. A disabled sensor stays disabled, a fence powered down for maintenance stays at zero volts, and an unnamed probe keeps its blank label - which is precisely the behaviour you want from a control room that must not invent readings.
Combining ?. and ??
In practice the two operators travel together: ?. gets you safely to the value, and ?? decides what to show when that value never arrives.
1const parkData = {
2 enclosures: {
3 "A-1": { species: "T-Rex", voltage: 10000 }
4 }
5};
6
7// A safe read with a default value
8const voltage = parkData.enclosures?.["B-2"]?.voltage ?? "No sensor";
9console.log("Voltage B-2:", voltage); // "No sensor"
10
11const rexVoltage = parkData.enclosures?.["A-1"]?.voltage ?? 0;
12console.log("Voltage A-1:", rexVoltage); // 10000Enclosure B-2 has no entry, so the chain yields undefined and ?? substitutes the label "No sensor" - the dashboard reports a gap in coverage instead of crashing. Enclosure A-1 answers with a real reading, and because 10000 is neither null nor undefined, the default is never used.
An error handling strategy
A good error handling strategy in an application works like the multi-level security system of Jurassic Park:
- Validate input data - check the data BEFORE it becomes a problem (like the checkpoint at the park gate)
- Use specific error types - create custom classes for different situations (different alarms for different threats)
- Handle errors at the right level - catch them where you can actually fix them (do not escalate a jammed feeder to the park director)
- Re-throw unknown errors - if you do not know how to handle an error, pass it up the chain
- Use finally to release resources - close connections, files and gates, whatever the outcome
Follow those five rules and remember the core idea: try/catch/finally is the park's emergency response protocol, and custom error classes are the specific alarms that tell the response team which procedure to run.
Code for this lesson: index.js
1// Error handling in JavaScript - Jurassic Park Edition
2console.log("=== JURASSIC PARK SECURITY SYSTEM ===\n");
3
4// ================================================
5// 1. TRY/CATCH - emergency protocol
6// ================================================
7console.log("--- 1. try/catch ---");
8
9try {
10 const dinoData = JSON.parse('{ "species": "T-Rex", "weight": 8500 }');
11 console.log("Dinosaur data:", dinoData.species, "-", dinoData.weight + "kg");
12} catch (error) {
13 console.log("ALARM! Data parsing error:", error.message);
14}
15
16// Error in parsing - catch catches the error
17try {
18 const brokenData = JSON.parse("{ bad format }");
19} catch (error) {
20 console.log("\nCaught error:", error.message);
21 console.log("Error type:", error.constructor.name);
22}
23
24// ================================================
25// 2. FINALLY - always executes
26// ================================================
27console.log("\n--- 2. try/catch/finally ---");
28
29function feedDinosaur(dinoName, food) {
30 console.log("\nOpening the feeder for " + dinoName + "...");
31 try {
32 if (!food) {
33 throw new Error("No food in the system!");
34 }
35 if (food === "grass" && dinoName === "Rex") {
36 throw new TypeError("T-Rex is not an herbivore!");
37 }
38 console.log("Giving " + food + " to " + dinoName + " - success!");
39 } catch (error) {
40 console.log("ALARM: " + error.message);
41 } finally {
42 console.log("Closing the feeder - safety!");
43 }
44}
45
46feedDinosaur("Rex", "meat");
47feedDinosaur("Rex", null);
48feedDinosaur("Rex", "grass");
49
50// ================================================
51// 3. ERROR TYPES
52// ================================================
53console.log("\n--- 3. Error types ---");
54
55// TypeError
56try {
57 const dino = null;
58 dino.roar();
59} catch (e) {
60 console.log("TypeError:", e.message);
61}
62
63// RangeError
64try {
65 const arr = new Array(-1);
66} catch (e) {
67 console.log("RangeError:", e.message);
68}
69
70// ReferenceError
71try {
72 console.log(nonExistentDinosaur);
73} catch (e) {
74 console.log("ReferenceError:", e.message);
75}
76
77// ================================================
78// 4. CUSTOM ERROR CLASSES
79// ================================================
80console.log("\n--- 4. Custom error classes ---");
81
82class DinosaurEscapeError extends Error {
83 constructor(species, enclosure) {
84 super("ALARM: " + species + " escaped from paddock " + enclosure + "!");
85 this.name = "DinosaurEscapeError";
86 this.species = species;
87 this.enclosure = enclosure;
88 }
89}
90
91class EnclosureVoltageError extends Error {
92 constructor(enclosure, voltage) {
93 super("Voltage in " + enclosure + ": " + voltage + "V (min 5000V)");
94 this.name = "EnclosureVoltageError";
95 this.enclosure = enclosure;
96 this.voltage = voltage;
97 }
98}
99
100const enclosures = [
101 { name: "A-1", species: "T-Rex", voltage: 10000, dinoEscaped: false },
102 { name: "B-3", species: "Raptor", voltage: 3000, dinoEscaped: false },
103 { name: "C-2", species: "Dilophosaurus", voltage: 8000, dinoEscaped: true },
104];
105
106for (const enc of enclosures) {
107 try {
108 if (enc.dinoEscaped) {
109 throw new DinosaurEscapeError(enc.species, enc.name);
110 }
111 if (enc.voltage < 5000) {
112 throw new EnclosureVoltageError(enc.name, enc.voltage);
113 }
114 console.log("Paddock " + enc.name + " - OK");
115 } catch (error) {
116 if (error instanceof DinosaurEscapeError) {
117 console.log("RED ALERT: " + error.message);
118 } else if (error instanceof EnclosureVoltageError) {
119 console.log("YELLOW ALERT: " + error.message);
120 }
121 }
122}
123
124// ================================================
125// 5. OPTIONAL CHAINING and NULLISH COALESCING
126// ================================================
127console.log("\n--- 5. ?. and ?? ---");
128
129const dinoRecord = {
130 species: "Velociraptor",
131 tracker: { lastLocation: "Sector C", gps: { lat: 23.5 } }
132};
133
134// Optional chaining - safe access
135console.log("GPS:", dinoRecord.tracker?.gps?.lat); // 23.5
136console.log("Medical:", dinoRecord.medical?.lastCheckup); // undefined
137
138// Nullish coalescing - default value
139const weight = 0;
140console.log("|| :", weight || "No data"); // "No data" (wrong!)
141console.log("??:", weight ?? "No data"); // 0 (correct!)
142
143// Combination
144const voltage = dinoRecord.tracker?.voltage ?? "No sensor";
145console.log("Voltage:", voltage);Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What is the correct way to handle an error that may occur during JSON parsing in JavaScript?
2. When does the finally block execute in a try/catch/finally construct?
3. What will the expression dinoRecord.medical?.lastCheckup?.date return if the dinoRecord object doesn't have a medical property?
Hands-on tasks in the game
- Code editor
Write a function findDinosaur(dinosaurs, id) that returns the dinosaur with the given id from the array and, when there is none, throws new Error('No dinosaur with ID ' + id). Write a function feedDinosaur(dinosaurs, id, food): in the try block find the dinosaur with findDinosaur, throw a TypeError when the food does not match the diet (carnivore eats 'meat', herbivore eats 'plants'), otherwise return true; in catch display error.message and return false; in finally display 'Closing the feeder'. Also write readVoltage(record) that safely reads record.sensors.voltage with the ?. operator and returns 'No sensor' when the value is missing (use ??).
- Vertical ordering
Arrange the stages of error handling in JavaScript from the moment the problem occurs:
- Click in order
Arrange the elements in the correct order to build a try/catch block in JavaScript:
- Code editor
Create the classes DinosaurEscapeError(species, enclosure), FeedingError(dinoName, reason) and SensorError(sensorType, enclosure) that extend Error, pass a message to super(), set this.name to the class name and store the parameters in properties with the same names. Write a function securityScan(enclosures) that, for each enclosure { name, species, voltage, dinoEscaped }, throws in a try block a DinosaurEscapeError when dinoEscaped is true or a SensorError('voltage', name) when voltage < 5000. In catch recognise the error with instanceof and display 'RED ALERT: ' + message or 'YELLOW ALERT: ' + message, and rethrow an error of any other type. The function returns an object with the number of alerts { red, yellow }.