Anatomy of an Error

Not all errors are the same. In JavaScript, you'll commonly encounter:

  • SyntaxError: You made a typo in your code, like a missing bracket. The code won't run at all.
  • ReferenceError: You tried to use a variable that hasn't been declared.
  • TypeError: You tried to perform an operation on the wrong type of value, like calling .toUpperCase() on a number.

These errors will stop your script's execution. Our goal is to handle them so our application doesn't crash.

Graceful Error Handling: try...catch...finally

The try...catch block is your primary tool for handling runtime errors.

  • try: The code inside this block is "monitored" for errors.
  • catch: If an error occurs in the try block, execution jumps to the catch block. The error object is passed as an argument.
  • finally: This block always runs, whether an error occurred or not. It's great for cleanup tasks.

Let's see it in action with a common scenario: parsing JSON that might be invalid.

JavaScript


const potentiallyBadJson = '{"name": "Alice", "age": 30,}'; // Extra comma is invalid!

try {
  console.log("Attempting to parse JSON...");
  const user = JSON.parse(potentiallyBadJson);
  console.log("Success! User:", user.name);
} catch (error) {
  // The code in 'try' failed, so this block runs.
  console.error("Oops, something went wrong while parsing!");
  console.error("Error details:", error.message); // e.g., "Unexpected token } in JSON at position..."
} finally {
  // This runs no matter what.
  console.log("JSON parsing routine finished.");
}

Without try...catch, the JSON.parse error would have crashed our script. With it, we caught the error and logged a helpful message.

Error Handling in Asynchronous Code

How you handle errors depends on whether you're using Promises with .then() or async/await.

1. With Promises (.then/.catch) You can chain a .catch() block at the end of a promise chain. It will catch any error that occurs in the fetch call or any preceding .then() block.

JavaScript


fetch('https://some-invalid-url.com/api/data')
  .then(response => {
    if (!response.ok) {
      // Create and throw an error to be caught below
      throw new Error('Network response was not OK');
    }
    return response.json();
  })
  .then(data => console.log(data))
  .catch(error => {
    // This will catch the network error or the thrown error
    console.error('Fetch operation failed:', error);
  });

2. With async/await async/await syntax allows you to use the familiar try...catch block for asynchronous code, which many find more readable.

JavaScript


async function fetchData() {
  try {
    const response = await fetch('https://some-invalid-url.com/api/data');
    if (!response.ok) {
      throw new Error('Network response was not OK');
    }
    const data = await response.json();
    console.log(data);
  } catch (error) {
    // Catches any error from the await calls or the thrown error
    console.error('Fetch operation failed:', error);
  }
}

fetchData();
Debugging 101: The Browser DevTools

console.log() is your best friend, but sometimes you need more power. The browser's developer tools are a game-changer.

  • console.log() / warn() / error(): Use them to inspect the value of variables at different points in your code. console.table() is great for arrays and objects.
  • The debugger; statement: Placing this keyword in your code will automatically pause execution when the browser's dev tools are open. This is called a breakpoint.
  • Breakpoints: When paused, you can:
  • Inspect variables: Hover over variables in the "Sources" panel to see their current value.
  • Step through code: Use the controls to run your code line-by-line to see exactly how it executes.
  • Use the Console: The console is live, allowing you to type in variable names and run commands in the current scope.