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.
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.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:
try block encounters an errorcatch block with the error objectcatch block handles the error (logging, recovery)finally block performs cleanup operations (resource cleanup)JavaScript ships with a built-in hierarchy of errors, much like the park has different alert levels for different kinds of threat.
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.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.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.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.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.
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 ?..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 species";
9 const weight = dino?.weight ?? "No data";
10 const isHealthy = dino?.isHealthy ?? false;
11 const speed = dino?.speed ?? 0;
12
13 return name + " | Weight: " + weight + "kg | 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.
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.A good error handling strategy in an application works like the multi-level security system of Jurassic Park:
Follow those five rules, @name, 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.