14 minLesson 28 of 35
OOP & Advanced Concepts
Error Handling with try/catch
Error Handling with try/catch
Robust JavaScript handles errors gracefully — it doesn't crash the entire application when something unexpected happens. This lesson covers synchronous and asynchronous error handling, custom error types, and defensive programming patterns.
try/catch/finally
try {
// Code that might throw
const data = JSON.parse(malformedJson);
processData(data);
} catch (error) {
// Runs when an error is thrown
console.error("Failed to parse:", error.message);
} finally {
// ALWAYS runs — error or not
closeConnection();
}
The Error Object
try {
null.toString(); // TypeError
} catch (e) {
e.name; // "TypeError"
e.message; // "Cannot read properties of null"
e.stack; // full stack trace (string)
}
// Built-in error types
new Error("generic error");
new TypeError("wrong type");
new RangeError("value out of range");
new SyntaxError("bad syntax");
new ReferenceError("variable not defined");
new URIError("bad URL encoding");
Custom Error Classes
class AppError extends Error {
constructor(message, statusCode = 500) {
super(message);
this.name = "AppError";
this.statusCode = statusCode;
}
}
class NotFoundError extends AppError {
constructor(resource) {
super(`${resource} not found`, 404);
this.name = "NotFoundError";
this.resource = resource;
}
}
class ValidationError extends AppError {
constructor(field, message) {
super(`Validation failed: ${message}`, 400);
this.name = "ValidationError";
this.field = field;
}
}
// Usage
function getUser(id) {
const user = users.find(u => u.id === id);
if (!user) throw new NotFoundError(`User #${id}`);
return user;
}
// Handle different error types
try {
const user = getUser(99);
} catch (err) {
if (err instanceof NotFoundError) {
showNotFound();
} else if (err instanceof ValidationError) {
showFieldError(err.field, err.message);
} else {
throw err; // re-throw unexpected errors
}
}
Async Error Handling
// async/await with try/catch
async function loadDashboard(userId) {
try {
const user = await fetchUser(userId);
const posts = await fetchPosts(userId);
return { user, posts };
} catch (err) {
if (err.status === 401) redirectToLogin();
else if (err.status === 404) throw new NotFoundError(`User #${userId}`);
else throw err; // unexpected errors bubble up
}
}
// Promise chains with .catch
fetchUser(userId)
.then(user => fetchPosts(user.id))
.then(posts => render(posts))
.catch(err => handleError(err));
// Unhandled promise rejections — catch globally
window.addEventListener("unhandledrejection", (event) => {
console.error("Unhandled promise rejection:", event.reason);
event.preventDefault(); // prevent default browser behavior
logToErrorService(event.reason);
});
Parallel Errors
// Promise.all — fails fast on any rejection
try {
const [user, posts, settings] = await Promise.all([
fetchUser(id),
fetchPosts(id),
fetchSettings(id)
]);
} catch (err) {
// Any one failure cancels all (others' results are discarded)
handleError(err);
}
// Promise.allSettled — handles mixed success/failure
const results = await Promise.allSettled([
fetchUser(id),
fetchPosts(id),
fetchSettings(id) // this one might fail
]);
results.forEach(result => {
if (result.status === "fulfilled") {
use(result.value);
} else {
console.error("Failed:", result.reason);
}
});
Defensive Patterns
// Result type pattern — avoid exceptions for expected failures
function parseJSON(str) {
try {
return { ok: true, value: JSON.parse(str) };
} catch {
return { ok: false, error: "Invalid JSON" };
}
}
const result = parseJSON(userInput);
if (result.ok) {
process(result.value);
} else {
showError(result.error);
}
// Retry with exponential backoff
async function fetchWithRetry(url, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (err) {
if (attempt === maxRetries - 1) throw err; // last attempt
const delay = 2 ** attempt * 1000; // 1s, 2s, 4s
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
Global Error Handlers
// Catch synchronous errors that escape try/catch
window.onerror = (message, source, line, col, error) => {
reportError({ message, source, line, col, stack: error?.stack });
return true; // prevents default browser error display
};
// Catch unhandled promise rejections
window.addEventListener("unhandledrejection", (event) => {
reportError(event.reason);
});
// In Node.js
process.on("uncaughtException", (err) => {
console.error("Uncaught exception:", err);
process.exit(1); // crash with non-zero code
});
process.on("unhandledRejection", (reason, promise) => {
console.error("Unhandled rejection:", reason);
});
When to Throw vs When to Return
// Throw for programmer errors (should never happen in correct code)
function getUser(id) {
if (typeof id !== "number") {
throw new TypeError(`Expected number, got ${typeof id}`);
}
// ...
}
// Return null/undefined for expected "not found" situations
function findUser(id) {
return users.find(u => u.id === id) ?? null;
}
// Return a result object for user-facing validation
function validateEmail(email) {
if (!email) return { valid: false, error: "Email is required" };
if (!/@/.test(email)) return { valid: false, error: "Must contain @" };
return { valid: true };
}
// Throw for unrecoverable failures that need to bubble up
async function getAuthenticatedUser() {
const token = localStorage.getItem("token");
if (!token) throw new AuthError("Not authenticated"); // calling code must handle
const res = await fetch("/api/me", { headers: { Authorization: `Bearer ${token}` } });
if (!res.ok) throw new AuthError("Token invalid");
return res.json();
}
Next lesson: JavaScript Design Patterns — reusable solutions to common problems.
📱
Get Notes Free →Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises