Gyaan

Error Handling

intermediate errors try-catch async debugging

Errors happen. APIs fail, users enter bad data, and things break. The key is to handle them gracefully so our app doesn’t crash and the user gets a useful message.

try/catch/finally

The try/catch block lets us attempt something and handle the error if it fails. The finally block runs no matter what.

try {
  const data = JSON.parse("not valid json");
} catch (error) {
  console.log("Parsing failed:", error.message);
} finally {
  console.log("This always runs"); // cleanup goes here
}

The finally block is perfect for cleanup — closing connections, hiding loading spinners, etc. It runs whether the try succeeded or failed.

The Error Object

When an error occurs, JavaScript creates an Error object with three useful properties:

  • message — the human-readable error description
  • name — the type of error (e.g., “TypeError”, “ReferenceError”)
  • stack — the full stack trace (which file, which line, the call chain)
try {
  undefined.foo;
} catch (error) {
  console.log(error.name);    // "TypeError"
  console.log(error.message); // "Cannot read properties of undefined"
  console.log(error.stack);   // full stack trace with file and line numbers
}

Throwing Errors

We can throw our own errors using throw. This is useful for validation and enforcing rules in our code.

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

try {
  divide(10, 0);
} catch (error) {
  console.log(error.message); // "Cannot divide by zero"
}

We can throw anything — a string, a number, an object — but it’s best practice to always throw an Error object so we get the stack trace.

Common Error Types

JavaScript has several built-in error types. Knowing what each one means helps with debugging:

  • TypeError — using a value the wrong way (calling non-function, accessing property of undefined)
  • ReferenceError — accessing a variable that doesn’t exist
  • SyntaxError — code has invalid syntax (missing bracket, bad token)
  • RangeError — a number is outside its valid range (array length -1, infinite recursion)
// TypeError
null.foo;                 // Cannot read properties of null
"hello"();                // "hello" is not a function

// ReferenceError
console.log(x);           // x is not defined

// RangeError
new Array(-1);             // Invalid array length

Custom Error Classes

For real applications, we often want our own error types so we can distinguish between different kinds of failures.

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

class NotFoundError extends Error {
  constructor(resource) {
    super(`${resource} not found`);
    this.name = "NotFoundError";
  }
}

// Usage
try {
  throw new ValidationError("email", "Invalid email format");
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(`${error.field}: ${error.message}`); // "email: Invalid email format"
  }
}

Using instanceof lets us catch specific error types and handle them differently.

Error Handling in Async Code

With Promises

Unhandled Promise rejections are one of the most common bugs. Always add a .catch().

fetch("/api/data")
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(error => console.log("Request failed:", error));

With async/await

Wrap await calls in try/catch — it works exactly like synchronous error handling.

async function loadUser(id) {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    const user = await response.json();
    return user;
  } catch (error) {
    console.log("Failed to load user:", error.message);
    return null; // return a fallback
  }
}

Global error catching

For errors that slip through, we can use global handlers:

// Browser — catches unhandled errors
window.addEventListener("error", (event) => {
  console.log("Uncaught error:", event.message);
});

// Catches unhandled Promise rejections
window.addEventListener("unhandledrejection", (event) => {
  console.log("Unhandled rejection:", event.reason);
});

Best practices

  • Always use Error objects (not strings) so you get a stack trace
  • Catch errors at the level where you can actually handle them
  • Don’t swallow errors silently — at least log them
  • Use custom error classes in larger apps for better error categorization
  • In async code, always handle rejections — unhandled rejections will crash Node.js

In simple language, try/catch is our safety net. We wrap risky code in try, handle failures in catch, and do cleanup in finally. For async code, use try/catch with async/await or .catch() with Promises. Throw custom errors when we need our code to fail loudly with a clear message.