1.8 Error Handling & Debugging
← Back to Chapter 1: JavaScript Fundamentals
Building resilient applications requires understanding how errors propagate through the call stack and event loop, how to handle unexpected failures gracefully, and how to master developer tooling for rapid troubleshooting.
1. Built-in Error Types in JavaScript
JavaScript provides a base Error class and several built-in sub-types representing specific runtime failures:
| Error Type | Common Cause | Example |
|---|---|---|
TypeError | Calling non-function or accessing property of null/undefined | null.name, (123)() |
ReferenceError | Referencing undeclared variable or accessing variable in TDZ | console.log(undeclaredVar) |
SyntaxError | Invalid JavaScript code syntax | const x = ; |
RangeError | Numeric value outside permissible range or maximum call stack exceeded | new Array(-1), infinite recursion |
URIError | Malformed parameters passed to decodeURIComponent() | decodeURIComponent('%') |
Anatomy of an Error Object
try {
throw new Error("Payment processing failed");
} catch (err) {
console.log(err.name); // "Error"
console.log(err.message); // "Payment processing failed"
console.log(err.stack); // Call stack trace indicating line numbers
}
2. Structured Exception Handling: try...catch...finally
function parseConfiguration(jsonString) {
try {
const config = JSON.parse(jsonString);
return config;
} catch (err) {
if (err instanceof SyntaxError) {
console.warn("Corrupt JSON provided, falling back to defaults.");
return { theme: "dark" };
}
// Re-throw unknown errors so they are not swallowed silently
throw err;
} finally {
console.log("Config parser run complete."); // Always executes!
}
}
try...catch cannot intercept errors thrown inside asynchronous callbacks or timers because the try block exits before the callback executes:
// ❌ This will crash your app unhandled:
try {
setTimeout(() => {
throw new Error("Boom!"); // Escapes try block!
}, 100);
} catch (err) {
// Never reached
}
// ✅ Correct: Handle inside the async callback or with async/await
async function safeAsync() {
try {
await riskyAsyncOperation();
} catch (err) {
console.error("Caught async failure:", err);
}
}
3. Creating Custom Error Hierarchies
Subclassing the base Error creates clean domain-specific errors for enterprise applications:
class ApiError extends Error {
constructor(statusCode, message, endpoint) {
super(message);
this.name = "ApiError";
this.statusCode = statusCode;
this.endpoint = endpoint;
// Capture accurate stack trace (V8 engine)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ApiError);
}
}
}
class ValidationError extends Error {
constructor(fields) {
super("Form validation failed");
this.name = "ValidationError";
this.fields = fields;
}
}
4. Debugging Techniques & Browser DevTools
A. The debugger; Statement
Placing debugger; in your source code creates a programmatic breakpoint that automatically pauses execution in browser DevTools if open:
function calculateScore(answers) {
let score = 0;
answers.forEach((ans, idx) => {
if (ans.isCorrect) score += 10;
debugger; // Pauses execution to inspect score and variables
});
return score;
}
B. Advanced console Methods
console.table(): Displays arrays of objects as clean formatted tables.console.group()/console.groupEnd(): Organizes related logs into collapsible groups.console.time(label)/console.timeEnd(label): Accurately measures elapsed execution time in milliseconds.
console.time("ArrayProcess");
const data = Array.from({ length: 10000 }, (_, i) => ({ id: i, val: i * 2 }));
console.table(data.slice(0, 5));
console.timeEnd("ArrayProcess");
5. React Error Boundaries
By default, an unhandled JavaScript error in a component tree unmounts the entire React app ("white screen of death"). Error Boundaries are React components that catch JavaScript errors anywhere in their child component tree and display a fallback UI.
import React, { Component } from 'react';
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
// Update state so the next render shows the fallback UI
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Log the error to an observability service (e.g. Sentry, Datadog)
console.error("ErrorBoundary caught an exception:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div style={{ padding: "20px", border: "1px solid red", borderRadius: "8px" }}>
<h2>⚠️ Something went wrong</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false })}>
Try Again
</button>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
6. Summary Checklist
- Know the core JavaScript error types:
TypeError,ReferenceError,SyntaxError,RangeError. - Always re-throw unrecognized errors inside
catchblocks to prevent silent bugs. - Handle asynchronous errors with
async/awaitand.catch(); timers require internal try-catches. - Use custom error classes to include HTTP status codes and domain context.
- Protect React applications with Error Boundaries around critical UI widgets and feature modules.