› Module 9: Error Handling › 9.2 Custom Errors
M O D U L E

Module 9: Error Handling

Failing gracefully instead of crashing the page

9.1 try / catch / finally

Uncaught exceptions immediately terminate script execution, abort ongoing background tasks, and leave user interfaces in broken, unresponsive states. JavaScript's try...catch...finally construct allows applications to anticipate potential failure points, intercept runtime exceptions, and perform deterministic resource cleanup.

Figure 9.1: JavaScript Exception Control Flow & Stack Unwinding try { parseData(); } Executes monitored code block throw Error catch (err) { handle(err); } Intercepts & handles exception finally { closeConnection(); } ALWAYS executes, error or not (even after return!)

Figure 9.1: If an error is thrown in try, execution immediately redirects to catch. In all scenarios, finally executes unconditionally before exiting.

1. Mechanics of the Exception Lifecycle

  • try: Wraps statements that could potentially fail (parsing JSON, accessing network payloads, reading undefined properties).
  • catch (error): Executes only if an exception is thrown inside the try block. Since ES2019, the error variable is optional (catch { ... }) if the error payload is unneeded.
  • finally: Executes unconditionally. It runs whether an error occurred or not, and even if return was called inside try or catch!

2. The Critical Asynchronous Trap

A classic placement interview gotcha: try...catch is completely synchronous. It cannot catch errors thrown inside asynchronous callbacks like setTimeout or unchained Promises because the try block has already finished executing and exited the call stack!

3. The finally Return Override Trap

If a finally block explicitly executes a return statement, that return value silently swallows and overrides any return value or pending uncaught exception from the try or catch block.

// 1. Synchronous Exception Interception
function parseUserData(rawJson) {
  try {
    const data = JSON.parse(rawJson);
    return data.user;
  } catch (err) {
    console.warn("JSON Parse Error:", err.message);
    return null; // Safe fallback value
  } finally {
    console.log("Cleanup: Parsing operation completed.");
  }
}

// 2. The Async Gotcha: WHY THIS FAILS:
try {
  setTimeout(() => {
    // throw new Error("Boom!"); // UNCAUGHT! try block already exited!
  }, 100);
} catch (err) {
  console.log("Never reaches here!");
}

// 3. The Correct Async Solution: async / await
async function fetchSecureData() {
  try {
    const res = await fetch("https://api.readytogo.dev/data");
    const json = await res.json();
    return json;
  } catch (err) {
    console.error("Caught async network error:", err.message);
  } finally {
    console.log("Hide loading spinner");
  }
}

9.2 Custom Errors

While JavaScript provides built-in error types (TypeError, ReferenceError, SyntaxError), production enterprise applications require domain-specific error classifications (e.g. ValidationError, AuthenticationError, DatabaseError) to differentiate operational failures from programming bugs and respond with precise HTTP status codes.

Figure 9.2: JavaScript Built-in Error Hierarchy & Custom Subclasses class Error (Root) name, message, stack Native JavaScript Subclasses TypeError • RangeError ReferenceError • SyntaxError Custom Application Classes class ValidationError class HttpError (404, 500) ES2022 Error Cause Chaining new Error(msg, { cause: err })

Figure 9.2: Subclassing the native Error class allows custom categories with metadata, clean stack traces, and cause chaining.

1. Subclassing the Built-in Error Class

A production custom error must satisfy three requirements:

  • Call super(message) to properly initialize the base Error instance and stack trace.
  • Explicitly assign this.name = this.constructor.name (by default, Error sets name = "Error").
  • In V8 runtimes (Node.js/Chrome), invoke Error.captureStackTrace(this, this.constructor) to keep the custom constructor out of the call stack trace.

2. Error Cause Chaining (ES2022)

When catching a low-level error (e.g., a raw database connection timeout), wrapping it in a domain error without losing the original stack trace used to be tedious. In ES2022, passing { cause: originalError } links the root failure directly to error.cause.

// 1. Defining a Domain-Specific HTTP Error Class
class HttpError extends Error {
  constructor(statusCode, message, options) {
    super(message, options); // Supports options.cause in ES2022!
    this.name = this.constructor.name;
    this.statusCode = statusCode;
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, this.constructor);
    }
  }
}

class NotFoundError extends HttpError {
  constructor(resource = "Resource", options) {
    super(404, resource + " was not found on this server.", options);
  }
}

// 2. Handling with instanceof Type Checking
function findUserById(id) {
  if (id !== 42) {
    throw new NotFoundError("User with ID " + id);
  }
  return { id: 42, username: "admin" };
}

try {
  findUserById(99);
} catch (err) {
  if (err instanceof NotFoundError) {
    console.warn("HTTP " + err.statusCode + ": " + err.message);
  } else {
    console.error("Unexpected runtime panic:", err);
  }
}
MODULE COMPREHENSIVE QUIZ

Module 9 Final Quiz

Test your understanding across all 2 topics of this module with interview-grade questions, tricky edge cases, and code output predictions.

Take Module 9 Final Quiz →
Yukti Mascot