Lesson 11: Error Handling - try, catch, throw

What You'll Learn

  • What errors are and why they occur
  • How to handle errors with try/catch
  • How to throw your own errors
  • Creating custom error classes
  • Error handling best practices
  • Finally block

Why This Matters

Errors happen in every program - a file doesn't exist, network fails, user enters invalid data. Good programs handle errors gracefully instead of crashing. Error handling makes your code robust and user-friendly.

---

Part 1: Understanding Errors

Create a new file: lesson-11/error-handling.js

What Happens When an Error Occurs?

console.log("Before error");
const result = 10 / 0;  // This creates Infinity, not an error

// But this will cause an error:
const user = null;
console.log(user.name);  // ❌ Error: Cannot read property 'name' of null

console.log("After error");  // This never runs!

When an error occurs:

  1. Program stops executing
  2. Error message is shown
  3. Code after the error never runs

---

Part 2: The try/catch Statement

try/catch lets you handle errors without crashing:

Basic Syntax

try {
  // Code that might cause an error
} catch (error) {
  // Code to run if error occurs
}

Simple Example

try {
  console.log("Trying something risky...");
  const user = null;
  console.log(user.name);  // This will error
  console.log("This line never runs");
} catch (error) {
  console.log("An error occurred!");
  console.log(error);
}

console.log("Program continues!");  // This DOES run!
Output:
Trying something risky...
An error occurred!
TypeError: Cannot read property 'name' of null
Program continues!

---

Part 3: Working with Error Objects

The catch block receives an error object:

try {
  const numbers = [1, 2, 3];
  console.log(numbers[10].toFixed(2));  // undefined.toFixed() causes error
} catch (error) {
  if (error instanceof Error) {
    console.log("Error name:", error.name);
    console.log("Error message:", error.message);
    console.log("Stack trace:", error.stack);
  }
}

Common Error Properties

  • name: Type of error (TypeError, ReferenceError, etc.)
  • message: Description of what went wrong
  • stack: Call stack (where error occurred)

---

Part 4: Throwing Your Own Errors

You can create errors with the throw keyword:

Basic throw

function divide(a, b) {
  if (b === 0) {
    throw new Error("Cannot divide by zero!");
  }
  return a / b;
}

try {
  console.log(divide(10, 2));  // 5
  console.log(divide(10, 0));  // Throws error
  console.log("This never runs");
} catch (error) {
  if (error instanceof Error) {
    console.log("Error:", error.message);
  }
}
Output:
5

Error: Cannot divide by zero!

When to Throw Errors

function getUser(id) {
  // Simulate database lookup
  const users = [
    { id: 1, name: "Alice" },
    { id: 2, name: "Bob" }
  ];
  
  const user = users.find(u => u.id === id);
  
  if (!user) {
    throw new Error(`User with id ${id} not found`);
  }
  
  return user;
}

try {
  const user = getUser(1);
  console.log(user.name);  // Alice
  
  const user2 = getUser(999);  // Throws error
  console.log(user2.name);
} catch (error) {
  if (error instanceof Error) {
    console.log("Error:", error.message);
  }
}

---

Part 5: Different Error Types

JavaScript has several built-in error types:

// TypeError - Wrong type
try {
  const num = "hello";
  num.toFixed(2);  // toFixed is for numbers, not strings
} catch (error) {
  console.log(error.name);  // TypeError
}

// ReferenceError - Variable doesn't exist
try {
  console.log(nonExistentVariable);
} catch (error) {
  console.log(error.name);  // ReferenceError
}

// RangeError - Value out of range
try {
  const arr = new Array(-1);  // Negative length
} catch (error) {
  console.log(error.name);  // RangeError
}

// SyntaxError - Invalid syntax (usually caught before running)
try {
  eval("const x = ;");  // Invalid syntax
} catch (error) {
  console.log(error.name);  // SyntaxError
}

Throwing Specific Error Types

function setAge(age) {
  if (age < 0 || age > 150) {
    throw new RangeError("Age must be between 0 and 150");
  }
  console.log(`Age set to ${age}`);
}

try {
  setAge(25);   // Age set to 25
  setAge(200);  // Throws RangeError
} catch (error) {
  if (error instanceof RangeError) {
    console.log("Range error:", error.message);
  }
}

---

Part 6: Custom Error Classes

Create your own error types for better organization:

class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = "ValidationError";
  }
}

class NotFoundError extends Error {
  constructor(message) {
    super(message);
    this.name = "NotFoundError";
  }
}

function validateEmail(email) {
  if (!email.includes("@")) {
    throw new ValidationError("Invalid email format");
  }
}

function findUser(id) {
  // Simulate database
  const users = [{ id: 1, name: "Alice" }];
  const user = users.find(u => u.id === id);
  
  if (!user) {
    throw new NotFoundError(`User ${id} not found`);
  }
  
  return user;
}

try {
  validateEmail("invalid-email");
} catch (error) {
  if (error instanceof ValidationError) {
    console.log("Validation failed:", error.message);
  } else if (error instanceof NotFoundError) {
    console.log("Not found:", error.message);
  } else {
    console.log("Unknown error:", error);
  }
}

Custom Error with Additional Data

class APIError extends Error {
  statusCode;
  endpoint;
  
  constructor(message, statusCode, endpoint) {
    super(message);
    this.name = "APIError";
    this.statusCode = statusCode;
    this.endpoint = endpoint;
  }
}

function fetchData(endpoint) {
  // Simulate API failure
  const success = false;
  
  if (!success) {
    throw new APIError(
      "Failed to fetch data",
      404,
      endpoint
    );
  }
}

try {
  fetchData("/api/users");
} catch (error) {
  if (error instanceof APIError) {
    console.log(`API Error at ${error.endpoint}`);
    console.log(`Status: ${error.statusCode}`);
    console.log(`Message: ${error.message}`);
  }
}

---

Part 7: The finally Block

finally runs whether an error occurs or not:
function processFile(filename) {
  console.log("Opening file...");
  
  try {
    console.log("Processing file...");
    if (filename === "error.txt") {
      throw new Error("File is corrupted");
    }
    console.log("File processed successfully");
  } catch (error) {
    console.log("Error processing file:", error.message);
  } finally {
    // This ALWAYS runs
    console.log("Closing file...");
  }
  
  console.log("Done!");
}

processFile("data.txt");
console.log("---");
processFile("error.txt");
Output:
Opening file...
Processing file...
File processed successfully
Closing file...
Done!
---
Opening file...
Processing file...
Error processing file: File is corrupted
Closing file...
Done!

When to Use finally

// Common use case: Cleanup resources
function connectToDatabase() {
  const connection = { connected: true };
  
  try {
    console.log("Connecting to database...");
    // Do database operations
    throw new Error("Connection failed");
  } catch (error) {
    console.log("Database error:", error.message);
  } finally {
    // Always cleanup, even if error occurred
    if (connection.connected) {
      console.log("Closing database connection");
      connection.connected = false;
    }
  }
}

connectToDatabase();

---

Part 8: Error Handling Best Practices

1. Be Specific with Error Messages

// ❌ Bad - Vague message
throw new Error("Invalid input");

// ✅ Good - Specific message
throw new Error("Email must contain @ symbol");

2. Handle Errors at the Right Level

// Handle errors where you can recover
function saveUser(user) {
  try {
    validateUser(user);
    // Save to database
  } catch (error) {
    // Handle validation error here
    console.log("Cannot save user:", error.message);
  }
}

// Let errors bubble up when you can't handle them
function validateUser(user) {
  if (!user.email) {
    throw new Error("Email is required");  // Let caller handle this
  }
}

3. Don't Catch and Ignore

// ❌ Bad - Swallowing errors
try {
  riskyOperation();
} catch (error) {
  // Empty catch - error is lost!
}

// ✅ Good - At least log it
try {
  riskyOperation();
} catch (error) {
  console.error("Error in riskyOperation:", error);
  // Maybe rethrow or handle
}

4. Validate Input Early

function calculateDiscount(price, percent) {
  // Validate first
  if (price < 0) {
    throw new Error("Price cannot be negative");
  }
  if (percent < 0 || percent > 100) {
    throw new Error("Percent must be between 0 and 100");
  }
  
  // Now calculate safely
  return price * (percent / 100);
}

---

Part 9: Practical Examples

Example 1: Safe JSON Parsing

function parseJSON(jsonString) {
  try {
    return JSON.parse(jsonString);
  } catch (error) {
    console.error("Invalid JSON:", error.message);
    return null;
  }
}

const validJSON = '{"name": "Alice", "age": 30}';
const invalidJSON = '{name: Alice}';  // Invalid JSON

const data1 = parseJSON(validJSON);
console.log(data1);  // { name: 'Alice', age: 30 }

const data2 = parseJSON(invalidJSON);
console.log(data2);  // null

Example 2: Form Validation

class ValidationError extends Error {
  field;
  
  constructor(field, message) {
    super(message);
    this.name = "ValidationError";
    this.field = field;
  }
}

function validateForm(data: FormData) {
  if (data.username.length < 3) {
    throw new ValidationError(
      "username",
      "Username must be at least 3 characters"
    );
  }
  
  if (!data.email.includes("@")) {
    throw new ValidationError(
      "email",
      "Email must contain @ symbol"
    );
  }
  
  if (data.password.length < 8) {
    throw new ValidationError(
      "password",
      "Password must be at least 8 characters"
    );
  }
}

function submitForm(data: FormData) {
  try {
    validateForm(data);
    console.log("Form submitted successfully!");
  } catch (error) {
    if (error instanceof ValidationError) {
      console.log(`Error in ${error.field}: ${error.message}`);
    } else {
      console.log("Unknown error:", error);
    }
  }
}

submitForm({
  username: "Al",
  email: "alice@example.com",
  password: "password123"
});
// Output: Error in username: Username must be at least 3 characters

Example 3: Retry Logic

async function fetchWithRetry(
  url,
  maxRetries = 3
) {
  let lastError: Error | null = null;
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      console.log(`Attempt ${attempt} of ${maxRetries}`);
      // Simulate fetch (replace with actual fetch)
      if (Math.random() < 0.7) {
        throw new Error("Network error");
      }
      return { data: "Success!" };
    } catch (error) {
      lastError = error as Error;
      console.log(`Attempt ${attempt} failed:`, error.message);
      
      if (attempt < maxRetries) {
        console.log("Retrying...");
        // In real code, add delay here
      }
    }
  }
  
  throw new Error(`Failed after ${maxRetries} attempts: ${lastError?.message}`);
}

// Use it
fetchWithRetry("/api/data")
  .then(result => console.log("Success:", result))
  .catch(error => console.log("Final error:", error.message));

---

Practice Exercises

Exercise 1: Safe Calculator

Create a calculator with error handling for division by zero, invalid inputs, etc.

Exercise 2: User Registration

Implement user registration with validation and custom error types.

Exercise 3: File Reader (Simulated)

Create a function that simulates file reading with error handling for missing files.

Exercise 4: API Call Handler

Create a function that handles API calls with retry logic and different error types.

Exercise 5: Password Validator

Create a comprehensive password validator with specific error messages.

---

Common Mistakes

Mistake 1: Catching Too Broadly

// ❌ Catches everything, even unexpected errors
try {
  manyOperations();
} catch (error) {
  console.log("Something failed");
}

// ✅ Be specific
try {
  riskyOperation();
} catch (error) {
  if (error instanceof ValidationError) {
    // Handle validation
  } else {
    // Rethrow unexpected errors
    throw error;
  }
}

Mistake 2: Forgetting Async Errors

// ❌ Doesn't catch promise rejections
try {
  fetch("/api/data");  // Returns promise
} catch (error) {
  // Never catches!
}

// ✅ Use async/await with try/catch
async function getData() {
  try {
    const response = await fetch("/api/data");
  } catch (error) {
    // This catches promise rejections
  }
}

---

Key Concepts Summary

Concept Purpose Example
try Code that might error try { risky(); }
catch Handle the error catch (error) { }
throw Create an error throw new Error("msg")
finally Always runs finally { cleanup(); }
Custom Error Specific error types class MyError extends Error

What You Learned

  • ✅ How errors stop program execution
  • ✅ How to catch errors with try/catch
  • ✅ How to throw your own errors
  • ✅ Different types of errors
  • ✅ How to create custom error classes
  • ✅ When to use the finally block
  • ✅ Error handling best practices
  • ✅ Real-world error handling patterns

What's Next?

In the next lesson, you'll learn about asynchronous programming - how to handle operations that take time, like fetching data from an API or reading files!