JS Errors & Exception Handling
Build resilient, crash-proof JavaScript applications. Master the anatomy of an Error object, standard
ECMAScript error types, structured try-catch-finally flow control, custom error hierarchies, error
re-throwing, and global unhandled exception monitoring.
Table of Contents
Anatomy of a JavaScript Error & The Call Stack
In JavaScript, an error is an object thrown to signal that an abnormal condition or runtime failure occurred.
All built-in error types inherit from the foundational Error.prototype and expose three essential properties:
Error Type Identifier
A string representing the class of error (e.g. "TypeError", "ReferenceError", or your custom class name).
Human-Readable Reason
A descriptive explanation detailing what caused the runtime violation.
Call Stack Trace
A chronological trace of all active stack frames, function calls, files, and line numbers leading to the error origin.
Code Example: Inspecting an Error Object
try {
// Deliberately calling a method on null
const profile = null;
profile.getUsername();
} catch (err) {
console.log("Error Name: ", err.name); // "TypeError"
console.log("Error Message:", err.message); // "Cannot read properties of null (reading 'getUsername')"
console.log("Stack Trace: \n", err.stack); // Full chronological call stack with line numbers
}
Try it in Playground
Standard ECMAScript Error Types
The ECMAScript specification defines six core standard Error constructors that inherit directly
from Error. Recognizing these types allows you to write precise diagnostic and recovery handlers:
| Error Class | When It Is Thrown | Common Real-World Example |
|---|---|---|
TypeError |
A value is not of the expected data type or a non-function is invoked. | null.toUpperCase() or const x = 5; x(); |
ReferenceError |
Attempting to access a variable that is undeclared or in the Temporal Dead Zone (TDZ). | console.log(undefinedVar); or accessing let before declaration. |
SyntaxError |
Code that violates JavaScript grammar rules (or invalid JSON in JSON.parse()). |
JSON.parse('{ bad: json }') or missing bracket. |
RangeError |
A numeric argument is outside its legal mathematical or engine range. | new Array(-5), (3.14).toFixed(200), or infinite recursion. |
URIError |
Global URI functions (decodeURI, decodeURIComponent) receive malformed escape sequences. |
decodeURIComponent('%') |
EvalError |
Historical error associated with eval() (rarely thrown in modern ES6+). |
Misuse of legacy evaluation contexts. |
Can `try...catch` Catch All SyntaxErrors?
No! If a SyntaxError occurs in raw source code (e.g. typing function( {),
the JavaScript engine throws it during the Parsing Phase before any line of code runs. Therefore,
a try...catch block around bad syntax cannot catch it. However, runtime syntax failures—such as
calling JSON.parse("{ bad json }")—can be trapped by try...catch!
Structured Exception Flow: try, catch, finally
The try...catch...finally construct allows you to intercept runtime errors and prevent your entire
application from abruptly crashing.
Contains the code that might fail. If no error occurs, execution flows through the end of try
and skips catch completely.
Executes only if an exception occurs inside try. Modern ES2019 allows optional catch binding
(catch { ... } without (err)).
Guaranteed to execute unconditionally, whether an error was thrown or not, even if the try
or catch block executes a return statement!
Code Example: The Unconditional Finally Guarantee
function processTransaction(account, amount) {
let isLocked = true;
console.log("1. Account locked for processing.");
try {
if (amount > account.balance) {
throw new RangeError("Insufficient funds: balance is " + account.balance);
}
account.balance -= amount;
console.log("2. Transaction successful. New balance:", account.balance);
return true; // Notice: returning from inside try!
} catch (err) {
console.warn("2. Transaction rejected:", err.message);
return false;
} finally {
// ALWAYS RUNS — perfect for closing database connections, unlocking mutexes, stopping loading spinners
isLocked = false;
console.log("3. FINALLY: Account unlocked safely. isLocked =", isLocked);
}
}
const userAccount = { balance: 250 };
processTransaction(userAccount, 100); // Successfully deducts and unlocks
processTransaction(userAccount, 500); // Traps RangeError, handles error, and unlocks
Try it in Playground
Throwing & Custom Error Hierarchies
The throw keyword interrupts standard execution and throws an exception. While JavaScript allows you
to throw any value (strings, numbers, objects), always throw an instance of Error
(or an extended subclass) so that call stack traces and prototype chains are preserved.
Code Example: Building an Enterprise Custom Error Hierarchy
// Base Application Error
class AppError extends Error {
constructor(message, statusCode = 500, errorCode = "INTERNAL_ERROR") {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
this.errorCode = errorCode;
this.timestamp = new Date().toISOString();
// Capture clean V8 stack trace excluding constructor frame
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
// Specialized Validation Error
class ValidationError extends AppError {
constructor(message, invalidFields = []) {
super(message, 400, "VALIDATION_FAILED");
this.invalidFields = invalidFields;
}
}
// Specialized Authentication Error
class AuthError extends AppError {
constructor(message = "Authentication required") {
super(message, 401, "UNAUTHORIZED");
}
}
// Usage in business logic
function registerUser(userData) {
if (!userData.email || !userData.email.includes("@")) {
throw new ValidationError("Invalid email provided", ["email"]);
}
if (!userData.token) {
throw new AuthError("Session expired, please log in again");
}
return { success: true, user: userData };
}
try {
registerUser({ email: "invalid-email" });
} catch (err) {
if (err instanceof ValidationError) {
console.error(`[${err.errorCode}] ${err.message} (Fields: ${err.invalidFields.join(", ")})`);
} else if (err instanceof AuthError) {
console.error(`[${err.errorCode}] Redirecting to login page...`);
} else {
console.error("Unknown system failure:", err);
}
}
Try it in Playground
Defensive Patterns & Global Error Handlers
Catching errors is only half the battle. Good architecture requires knowing when to re-throw unexpected errors and how to monitor global unhandled rejections:
The Error Re-Throwing Pattern
Only catch errors that your function knows how to recover from. If an error is of an unexpected type
(e.g. an unexpected ReferenceError due to a typo), re-throw it so higher-level
handlers or telemetry catch it.
Global Safety Nets
In modern browsers, register window.onerror for uncaught sync errors and
unhandledrejection for unhandled Promise rejections to send telemetry to monitoring services (like Sentry).
Code Example: Re-throwing & Global Telemetry
// 1. Re-throwing unknown errors
function loadUserData(jsonStr) {
try {
const data = JSON.parse(jsonStr);
return data.user;
} catch (err) {
if (err instanceof SyntaxError) {
console.warn("Recovering from corrupted cache JSON. Using empty default.");
return null;
}
// Unknown bug: Re-throw to caller!
throw err;
}
}
// 2. Global Unhandled Rejection Monitor (Browser)
window.addEventListener("unhandledrejection", (event) => {
console.error("TELEMETRY ALERT: Unhandled Promise Rejection:", event.reason);
// Send to server logging API
// fetch('/api/logs', { method: 'POST', body: JSON.stringify({ error: event.reason.message }) });
});
Try it in Playground
Interactive Practice Labs
Lab 1: Standard Error Trigger & Stack Inspector
Interactive Sandbox
Trigger different built-in JavaScript error classes and observe how their name, message,
and call stack are formatted:
Lab 2: Try-Catch-Finally Execution Flow Tracer
Toggle whether an error is thrown inside the try block and watch the deterministic execution path unfold:
Hands-on Challenge & Chapter Quiz
Objective: Write a bulletproof utility function safeJsonParse(jsonString, fallbackValue)
that:
1. Traps any SyntaxError or TypeError without crashing.
2. Returns the parsed object if valid, or fallbackValue if parsing fails.
3. Logs a structured warning metric containing the error message and timestamp.
Clean Solution:
function safeJsonParse(jsonString, fallbackValue = null) {
// Defensive guard against non-string input
if (typeof jsonString !== "string") {
console.warn(`[SafeJsonParse] Invalid input type: expected string, received ${typeof jsonString}`);
return fallbackValue;
}
try {
return JSON.parse(jsonString);
} catch (err) {
console.warn(`[SafeJsonParse] Parsing failed at ${new Date().toISOString()}: ${err.message}`);
return fallbackValue;
}
}
// Verification:
console.log(safeJsonParse('{"user":"Sanjay","role":"Admin"}')); // { user: "Sanjay", role: "Admin" }
console.log(safeJsonParse('{ corrupt json }', {})); // Traps SyntaxError and returns {}
console.log(safeJsonParse(12345, [])); // Traps non-string and returns []
Chapter 16 Knowledge Check
1. What error type is thrown when executing const x = null; x.someMethod();?
2. What happens if a function executes return "from try" inside try, but also defines a finally block?
3. What is the return value of accessing a variable declared with let before its line of initialization?
4. Why is throwing primitive strings (e.g. throw "error") considered bad practice?
5. Which browser event intercepts unhandled Promise rejections globally?
window.onpromiseerrorwindow.addEventListener('unhandledrejection')document.onasyncerrorPromise.catchAll()