JavaScript Error Handling

Errors are inevitable in any program. A user might enter invalid data, a network request might fail, or a function might be called with unexpected arguments. Proper error handling allows programs to respond to these situations gracefully instead of crashing.

Types of JavaScript Errors

Error TypeCauseExample
SyntaxErrorInvalid code syntax — caught before runningMissing bracket or quote
ReferenceErrorUsing a variable that doesn't existconsole.log(x) where x is not declared
TypeErrorWrong data type used in an operationnull.toUpperCase()
RangeErrorValue is out of allowed rangenew Array(-1)
URIErrorMalformed URI useddecodeURI('%')

try...catch — The Core of Error Handling

The try...catch statement lets code attempt a potentially risky operation and catch any errors that occur, preventing the program from crashing.

Syntax:

try {
  // Code that might cause an error
} catch (error) {
  // Code that runs if an error occurs
}
try {
  let result = undefined.toUpperCase();  // TypeError!
} catch (error) {
  console.log("Error caught:", error.message);
  // Output: Cannot read properties of undefined (reading 'toUpperCase')
}

Without try...catch, this error would stop the entire script. With it, execution continues after the catch block.

The Error Object

The error object inside catch contains useful information about what went wrong.

try {
  let data = JSON.parse("{invalid json}");
} catch (error) {
  console.log("Name:", error.name);       // SyntaxError
  console.log("Message:", error.message); // Unexpected token i in JSON...
  console.log("Stack:", error.stack);     // Full error trace
}

The finally Block

The finally block always runs — whether an error occurred or not. It is used for cleanup tasks like closing a connection or hiding a loading spinner.

function loadData() {
  try {
    console.log("Loading data...");
    // Simulating an error
    throw new Error("Network failure!");
  } catch (error) {
    console.log("Failed:", error.message);
  } finally {
    console.log("Cleanup: hiding loader.");  // Always runs
  }
}

loadData();
// Loading data...
// Failed: Network failure!
// Cleanup: hiding loader.

Throwing Custom Errors

The throw statement creates and throws a custom error manually. This is useful for enforcing business rules and input validation.

function divide(a, b) {
  if (b === 0) {
    throw new Error("Division by zero is not allowed.");
  }
  return a / b;
}

try {
  let result = divide(10, 0);
  console.log(result);
} catch (error) {
  console.log("Error:", error.message);
}
// Error: Division by zero is not allowed.

Throwing Different Error Types

function setAge(age) {
  if (typeof age !== "number") {
    throw new TypeError("Age must be a number.");
  }
  if (age < 0 || age > 150) {
    throw new RangeError("Age must be between 0 and 150.");
  }
  return age;
}

try {
  setAge("twenty");
} catch (error) {
  if (error instanceof TypeError) {
    console.log("Type issue:", error.message);
  } else if (error instanceof RangeError) {
    console.log("Range issue:", error.message);
  }
}
// Type issue: Age must be a number.

Custom Error Classes

In larger applications, custom error classes allow creating specific error types that can be caught and handled separately.

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

function validateUser(user) {
  if (!user.name) {
    throw new ValidationError("Name is required.", "name");
  }
  if (!user.email || !user.email.includes("@")) {
    throw new ValidationError("Invalid email address.", "email");
  }
  return true;
}

try {
  validateUser({ name: "", email: "notanemail" });
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(`Field "${error.field}" has error: ${error.message}`);
  }
}
// Field "name" has error: Name is required.

Error Handling in Asynchronous Code

With Promises

fetch("https://api.example.com/data")
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.log("Fetch failed:", error.message))
  .finally(() => console.log("Request complete."));

With async/await

async function getData() {
  try {
    let response = await fetch("https://api.example.com/data");
    let data = await response.json();
    console.log(data);
  } catch (error) {
    console.log("Failed to fetch:", error.message);
  } finally {
    console.log("Done.");
  }
}

getData();

Defensive Programming with Validation

Combining error handling with input validation prevents most runtime errors from occurring in the first place.

function calculateDiscount(price, percent) {
  if (typeof price !== "number" || typeof percent !== "number") {
    throw new TypeError("Both price and percent must be numbers.");
  }

  if (price < 0) {
    throw new RangeError("Price cannot be negative.");
  }

  if (percent < 0 || percent > 100) {
    throw new RangeError("Discount percent must be between 0 and 100.");
  }

  return price - (price * percent / 100);
}

try {
  let finalPrice = calculateDiscount(1000, 20);
  console.log("Discounted Price:", finalPrice);  // 800
} catch (error) {
  console.log(error.name + ":", error.message);
}

try...catch...finally Structure

BlockPurposeWhen It Runs
tryCode that might cause an errorAlways, first
catchHandle the error if one occursOnly when an error is thrown
finallyCleanup codeAlways, last (error or not)

Key Points to Remember

  • Use try...catch to handle errors gracefully without crashing the program
  • The error object in the catch block has name, message, and stack properties
  • The finally block always runs — use it for cleanup tasks
  • Use throw to create and raise custom errors manually
  • Use instanceof to check the type of a caught error and handle different errors differently
  • Create custom error classes by extending the built-in Error class
  • In async/await code, wrap await calls in try/catch to catch promise rejections

Leave a Comment

Your email address will not be published. Required fields are marked *