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.