Week 11: Handling Errors and Debugging Code

Learn how to anticipate, catch, and fix errors in your JavaScript programs.

Explore Chapter 11

Chapter 11: Dealing with the Unexpected

Types of Errors in JavaScript.

Errors are a normal part of software development. Understanding the different kinds of errors that can occur in JavaScript helps you diagnose and fix them more effectively.

1. Syntax Errors

These errors occur when your code violates the grammatical rules of the JavaScript language. The browser's JavaScript engine cannot parse or understand the code. Syntax errors prevent the script from executing at all.

// Example: Missing closing parenthesis
// console.log("Hello"

// Example: Misspelled keyword
// functon greet() {}

// Example: Missing curly brace
// if (x > 5) { console.log("Hi");

Syntax errors are usually caught during the initial parsing phase and reported in the browser's developer console, often pointing to the line where the error occurred.

2. Runtime Errors (Exceptions)

These errors occur *during* the execution of the script, after the syntax has been successfully parsed. They happen when the engine encounters an operation it cannot perform.

Common types of runtime errors (often represented by specific Error objects):

  • ReferenceError: Occurs when trying to access a variable or function that hasn't been declared or is not in scope.
    // console.log(nonExistentVariable); // ReferenceError
    
  • TypeError: Occurs when an operation is performed on a value of an inappropriate type (e.g., calling something that isn't a function, accessing properties of null or undefined).
    // let num = 10;
    // num.toUpperCase(); // TypeError: num.toUpperCase is not a function
    
    // let data = null;
    // console.log(data.property); // TypeError: Cannot read properties of null
    
  • RangeError: Occurs when a value is not in the set or range of allowed values (e.g., invalid array length).
    // let arr = new Array(-1); // RangeError: Invalid array length
    
  • URIError: Occurs when global URI handling functions (like decodeURIComponent()) are used incorrectly.

Runtime errors will stop the execution of the current script unless they are handled using error handling mechanisms.

3. Logical Errors

These are errors in the program's logic. The code runs without throwing syntax or runtime errors, but it doesn't produce the expected or correct result. These are often the hardest errors to find and fix, requiring careful debugging.

// Example: Incorrect calculation logic
function calculateAverage(a, b) {
  // Bug: Should be (a + b) / 2
  return a + b / 2; // Divides b by 2 first, then adds a
}
console.log(calculateAverage(4, 6)); // Expected 5, but gets 7

Using Browser Debugger Tools.

Modern web browsers come with powerful built-in developer tools that include a debugger. Learning to use the debugger is an essential skill for finding and fixing errors (especially runtime and logical errors).

Key Debugger Features:

  • Console: We've already used console.log() to print values. The console also displays error messages and allows you to execute JavaScript commands interactively.
  • Sources Panel: Allows you to view the source code of your HTML, CSS, and JavaScript files.
  • Breakpoints: You can set breakpoints in your JavaScript code (usually by clicking on the line number in the Sources panel). When the code execution reaches a breakpoint, it pauses, allowing you to inspect the state of your program.
  • Stepping Controls: While paused at a breakpoint, you can:
    • Step Over: Execute the current line and move to the next line (without going inside any function calls on the current line).
    • Step Into: If the current line calls a function, move execution into that function.
    • Step Out: Continue execution until the current function returns, then pause again.
    • Resume: Continue execution normally until the next breakpoint (or the end of the script).
  • Watch Expressions: Monitor the values of specific variables or expressions as you step through the code.
  • Call Stack: Shows the sequence of function calls that led to the current point of execution. Helps understand how you got there.
  • Scope Variables: Inspect the values of local and global variables at the current point of execution.

How to Use:

  1. Open Developer Tools (F12 or right-click -> Inspect).
  2. Go to the "Sources" (or sometimes "Debugger") tab.
  3. Find your JavaScript file in the file navigator.
  4. Click on a line number to set a breakpoint.
  5. Refresh the page or perform the action that triggers the code containing the breakpoint.
  6. Execution will pause. Use the stepping controls and inspect variables/watch expressions to understand what's happening.

Using the debugger is much more efficient than littering your code with console.log statements for complex problems.

Exception Handling using try...catch...finally.

JavaScript provides the try...catch...finally statement to handle runtime errors (exceptions) gracefully, preventing them from crashing your entire script.

Syntax

try {
  // Code that might potentially throw an error
  // ... risky operations ...
  console.log("Attempting risky operation...");
  let result = riskyFunction(); // Assume this might fail
  console.log("Operation succeeded.");

} catch (error) {
  // Code to execute if an error occurs within the 'try' block
  console.error("An error occurred!");
  console.error("Error name:", error.name);     // e.g., "TypeError"
  console.error("Error message:", error.message); // Specific error description
  // console.error(error.stack); // Stack trace (more detail)
  // You can decide how to handle the error here (log it, show user message, etc.)

} finally {
  // Code that will always execute, regardless of whether an error occurred or not
  // Useful for cleanup operations (e.g., closing resources, resetting state)
  console.log("Finally block executed - cleanup can happen here.");
}
  • try block: Contains the code that might throw an exception.
  • catch (error) block: If an error occurs in the try block, execution jumps to the catch block. The error object contains information about the error (like its name and message). This block is optional (but usually needed if you use try).
  • finally block: This block is optional. Its code executes *after* the try (and catch, if an error occurred) block finishes, regardless of whether an error happened. It's useful for cleanup actions that must always run.

Example

function divide(a, b) {
  if (b === 0) {
    // We'll learn to throw errors next
    return "Error: Division by zero is not allowed.";
  }
  return a / b;
}

try {
  let num1 = 10;
  let num2 = 0; // Change to non-zero to see success path
  console.log("Trying division...");
  let result = divide(num1, num2);
  console.log("Result:", result); // This might not run if error occurs before

} catch (err) {
  // This specific example doesn't throw a catchable error yet,
  // but shows where one *would* be caught.
  console.error("Caught an error:", err.message);

} finally {
  console.log("Division attempt finished.");
}

Using try...catch allows your program to handle errors gracefully instead of abruptly stopping.

The throw Statement: Creating Custom Errors.

In addition to catching built-in runtime errors, you can also explicitly signal that an error condition has occurred in your own code using the throw statement.

You can throw various types of values, but it's standard practice to throw Error objects (or objects inheriting from Error).

Syntax

throw expression; // The value/object to be thrown

Throwing Error Objects

JavaScript has built-in error constructors (Error, TypeError, ReferenceError, etc.) that you can use:

function checkAge(age) {
  if (typeof age !== 'number') {
    throw new TypeError("Age must be a number."); // Throw a specific error type
  }
  if (age < 0) {
    throw new Error("Age cannot be negative."); // Throw a generic error object
  }
  if (age < 18) {
    console.log("Access denied - under 18.");
    return false;
  }
  console.log("Access granted.");
  return true;
}

try {
  console.log("Checking age 25...");
  checkAge(25); // Access granted.

  console.log("Checking age -5...");
  checkAge(-5); // This will throw an Error

  // This line won't be reached if the previous one throws an error
  console.log("Checking age 'twenty'...");
  checkAge("twenty");

} catch (error) {
  console.error("--- Error Caught ---");
  console.error("Name:", error.name);
  console.error("Message:", error.message);
  console.error("--------------------");
} finally {
    console.log("Age checks complete.");
}

When an error is thrown, the current function stops executing, and the JavaScript engine looks up the call stack for the nearest catch block to handle it. If no catch block is found, the program terminates (usually logging the error to the console).

Throwing custom errors makes your functions more robust by clearly indicating when invalid input or unexpected situations occur.

Syllabus