Lesson 116 min read

Error Handling

When things go wrong (and they will), catch the problem before it catches you

Why Errors Happen (And Why That's OK)

Errors are a normal part of programming. A user types something unexpected. A server goes down. A file doesn't exist. Good code doesn't prevent all errors — it handles them gracefully.

Think of error handling like a safety net under a trapeze. The acrobat (your code) does amazing things up high, and the net (error handling) catches them if they fall. Without it, one mistake crashes the whole show.

JavaScript has several built-in error types:

  • TypeError — doing something a value's type doesn't support (like calling null.toString())
  • ReferenceError — using a variable that doesn't exist
  • SyntaxError — writing code JavaScript can't parse
  • RangeError — a number outside the allowed range
  • URIError — bad URI encoding
The try/catch/finally control flow: the catch block only runs if an error is thrown, but the finally block always executes regardless.

Try / Catch / Finally

// Basic try/catch
try {
let data = JSON.parse("not valid json");
console.log(data);
} catch (error) {
console.log("Caught an error!");
console.log(error.name); // "SyntaxError"
console.log(error.message); // "Unexpected token 'o'..."
}
// finally — always runs, error or not
function readFile(name) {
console.log(`Opening ${name}...`);
try {
if (name === "missing.txt") {
throw new Error("File not found!");
}
console.log(`Reading ${name}... done!`);
return "file contents";
} catch (error) {
console.log(`Error: ${error.message}`);
return null;
} finally {
console.log(`Closing ${name}. (always runs)`);
}
}
readFile("data.txt");
console.log("---");
readFile("missing.txt");
Output
Caught an error!
SyntaxError
Unexpected token 'o', "not valid json" is not valid JSON
Opening data.txt...
Reading data.txt... done!
Closing data.txt. (always runs)
---
Opening missing.txt...
Error: File not found!
Closing missing.txt. (always runs)

Throwing Your Own Errors

You're not limited to catching JavaScript's built-in errors — you can throw your own. Use throw to signal that something went wrong in your code. This is like pulling the fire alarm when you spot a problem.

You can throw anything (a string, a number), but best practice is to throw an Error object because it includes a stack trace — a breadcrumb trail showing exactly where the error came from.

Custom Errors & Validation

// Throwing errors for validation
function divide(a, b) {
if (typeof a !== "number" || typeof b !== "number") {
throw new TypeError("Both arguments must be numbers");
}
if (b === 0) {
throw new RangeError("Cannot divide by zero");
}
return a / b;
}
try {
console.log(divide(10, 2)); // 5
console.log(divide(10, 0)); // throws!
} catch (e) {
console.log(`${e.name}: ${e.message}`);
}
// Custom error class
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
function createUser(name, age) {
if (!name) throw new ValidationError("name", "Name is required");
if (age < 0) throw new ValidationError("age", "Age cannot be negative");
return { name, age };
}
try {
createUser("", 25);
} catch (e) {
if (e instanceof ValidationError) {
console.log(`Validation failed on '${e.field}': ${e.message}`);
} else {
throw e; // re-throw unexpected errors
}
}
Output
5
RangeError: Cannot divide by zero
Validation failed on 'name': Name is required

Error Handling Patterns

In real-world code, you'll often see these patterns:

  • Catch and recover — handle the error and continue with a fallback
  • Catch and log — log the error for debugging but let the app continue
  • Catch and re-throw — catch specific errors you can handle, re-throw the rest
  • Guard clauses — check for problems early with if statements before they become errors

A good rule of thumb: use try/catch around code that interacts with the outside world — network requests, file operations, JSON parsing, user input. For your own logic, prefer guard clauses and validation.

Real-World Pattern: Safe JSON Parsing

// A reusable safe parser
function safeJsonParse(text, fallback = null) {
try {
return JSON.parse(text);
} catch {
return fallback;
}
}
// Works perfectly with valid JSON
let data = safeJsonParse('{"name": "Luna", "level": 5}');
console.log(data); // { name: "Luna", level: 5 }
// Returns fallback for invalid JSON instead of crashing
let bad = safeJsonParse("not json", { name: "Unknown" });
console.log(bad); // { name: "Unknown" }
// Guard clause pattern — validate early, fail fast
function processOrder(order) {
if (!order) throw new Error("Order is required");
if (!order.items?.length) throw new Error("Order must have items");
if (order.total < 0) throw new Error("Total cannot be negative");
// If we get here, we know the data is good!
console.log(`Processing order: ${order.items.length} items, $${order.total}`);
}
try {
processOrder({ items: ["book", "pen"], total: 24.99 });
processOrder({ items: [], total: 0 });
} catch (e) {
console.log(`Failed: ${e.message}`);
}
Output
{ name: "Luna", level: 5 }
{ name: "Unknown" }
Processing order: 2 items, $24.99
Failed: Order must have items
Note: Never use an empty catch block: catch (e) { }. This is called "swallowing" errors, and it makes debugging a nightmare because problems happen silently. At the very least, log the error: catch (e) { console.error(e); }. Future-you will thank present-you.

Quick check

When does the 'finally' block run?

Continue reading

StackData Structure
LIFO — array & linked-list backed
Error HandlingPython
Catch mistakes before they crash your program
Exception HandlingJava
try/catch, checked vs unchecked, and throws
DOM ManipulationClasses & OOP