We use cookies to enhance your experience on the site
CodeWorlds

Error Handling in JavaScript

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:

  1. Code in the
    try
    block encounters an error
  2. Execution passes to the
    catch
    block with the error object
  3. The
    catch
    block handles the error (logging, recovery)
  4. The
    finally
    block performs cleanup operations (resource cleanup)
  5. The program continues execution after the try/catch/finally block

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 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 throw

The 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); // 10000

Enclosure 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:

  1. Validate input data - check the data BEFORE it becomes a problem (like the checkpoint at the park gate)
  2. Use specific error types - create custom classes for different situations (different alarms for different threats)
  3. Handle errors at the right level - catch them where you can actually fix them (do not escalate a jammed feeder to the park director)
  4. Re-throw unknown errors - if you do not know how to handle an error, pass it up the chain
  5. Use finally to release resources - close connections, files and gates, whatever the outcome

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.

Go to CodeWorlds