Error Handling: Custom Classes & Global Handlers
try/catch se sync errors, .catch() se Promise errors, finally se cleanup. Custom Error classes banao. Global handlers lagao. Koi error chhupe nahi.
The Error object is JavaScript's built-in mechanism for representing errors. new Error(message) creates an error with three key properties: message (the error description), name (defaults to "Error"), and stack (the call stack string at the point of creation).
JavaScript provides built-in error subclasses for common error types:
• TypeError — operation on a value of the wrong type (e.g., calling a non-function)
• RangeError — value is out of the allowed range (e.g., new Array(-1))
• ReferenceError — accessing an undeclared variable
• SyntaxError — code cannot be parsed (caught at parse time, not runtime)
• URIError — malformed URI passed to encodeURI() or decodeURI()
// Built-in error types
try { null.x; } catch (e) { console.log(e instanceof TypeError); } // true
try { new Array(-1); } catch (e) { console.log(e instanceof RangeError); } // true
try { undeclaredVar; } catch (e) { console.log(e instanceof ReferenceError); } // true
// Always throw Error objects, not strings
function validateAge(age) {
if (typeof age !== 'number') {
// ❌ throw 'Age must be a number'; — No stack trace!
// ✅ Always throw Error objects:
throw new TypeError('Age must be a number, got ' + typeof age);
}
if (age < 0 || age > 150) {
throw new RangeError('Age must be between 0 and 150');
}
}
try {
validateAge('twenty');
} catch (err) {
if (err instanceof TypeError) {
console.log('Type error:', err.message);
} else if (err instanceof RangeError) {
console.log('Range error:', err.message);
}
}
Why throw Error objects, not strings: throw 'message' works but gives you no stack trace. throw new Error('message') captures the call stack at the throw point — invaluable for debugging. Use err.constructor.name or err instanceof TypeError to check specific error types.
Extend Error to create custom error types with specific properties and behavior. This lets you handle different error categories differently — validation errors need different treatment than network errors.
Must-do rules for custom errors: (1) Call super(message) in constructor. (2) Set this.name = 'CustomError' — without it, err.name stays "Error" from the parent. (3) Add custom properties: field, statusCode, url — any metadata you need.
// Base application error
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.name = 'AppError';
this.statusCode = statusCode;
}
}
// Validation error — user input problems
class ValidationError extends AppError {
constructor(field, message) {
super(message, 400);
this.name = 'ValidationError';
this.field = field;
}
}
// Network error — API failures
class NetworkError extends AppError {
constructor(url, statusCode, message) {
super(message, statusCode);
this.name = 'NetworkError';
this.url = url;
}
}
// Auth error — permission problems
class AuthError extends AppError {
constructor(message) {
super(message, 401);
this.name = 'AuthError';
}
}
// Usage — specific error handling
try {
const user = await fetchUser(1);
} catch (err) {
if (err instanceof ValidationError) {
showFieldError(err.field, err.message);
} else if (err instanceof AuthError) {
redirectToLogin();
} else if (err instanceof NetworkError) {
showRetryDialog(err.url);
} else {
console.error(err);
showGenericError();
}
}
Why custom error classes: instanceof checks work — err instanceof ValidationError is true. Stack trace is automatically captured when super() is called. You can add any property you need for specific handling logic.
try/catch/finally is the sync error handling trifecta: try { risky code } → catch(err) { handle error } → finally { cleanup — always runs }.
finally runs even if try/catch returns or throws — it ALWAYS executes. Use it for cleanup: closing connections, hiding spinners, releasing resources.
// finally always runs — even on return/throw
function getData() {
try {
const data = riskyOperation();
return data; // Return from try
} catch (err) {
return null; // Return from catch
} finally {
// This ALWAYS runs — even after the returns above!
cleanupResources();
// ❌ NEVER return from finally — it overrides try/catch returns!
// return 'override'; // Would override the return above!
}
}
// Promise error propagation
fetch('/api/user')
.then(res => res.json())
.then(user => fetch('/api/orders?userId=' + user.id))
.then(res => res.json())
.then(orders => console.log(orders))
.catch(err => {
// Catches rejection from ANY .then above!
console.error('Chain error:', err);
})
.finally(() => {
hideLoadingSpinner(); // Always clean up
});
// ❌ NEVER swallow errors silently
try {
riskyOperation();
} catch (err) {
// Empty catch! Error disappears. Debugging nightmare.
}
// ✅ At minimum, log the error
try {
riskyOperation();
} catch (err) {
console.error('Operation failed:', err);
// Or re-throw if you can't handle it
// throw err;
}
.then(f, r) — handler r only catches the original promise rejection, NOT errors from f. .then(f).catch(r) — .catch(r) catches errors from BOTH the promise AND handler f. Always use .then(f).catch(r) for full error coverage.Some errors escape all try/catch and .catch() blocks. Global error handlers are your last resort — they catch errors that would otherwise crash silently.
window.onerror catches sync errors in event handlers and timers. window.addEventListener('unhandledrejection', fn) catches unhandled Promise rejections.
// Global sync error handler
window.onerror = function(message, source, lineno, colno, error) {
console.error('Global error caught:', {
message, source, lineno, colno,
stack: error?.stack
});
// Send to error monitoring service
// logToService({ message, source, lineno, colno, stack: error?.stack });
return false; // false = still show in console. true = suppress.
};
// Global unhandled promise rejection handler
window.addEventListener('unhandledrejection', function(event) {
console.error('Unhandled rejection:', event.reason);
// event.reason = the rejection value
// event.promise = the promise that was rejected
// Log to monitoring service
// logToService({ type: 'unhandledrejection', reason: event.reason });
// Prevent default console warning (optional)
// event.preventDefault();
});
// These catch errors that escape your code:
setTimeout(() => {
throw new Error('Error in timer!'); // Caught by window.onerror
}, 1000);
Promise.reject('Uncaught rejection!'); // Caught by unhandledrejection
When to use global handlers: Error logging to external services (Sentry, LogRocket), showing user-friendly error messages, catching bugs in production that slipped past local try/catch blocks.
A solid error handling strategy layers multiple techniques — try/catch for sync, .catch() for promises, custom classes for specific handling, and global handlers as safety net.
// Complete error handling strategy
class AppError extends Error {
constructor(message, { code, details, userMessage } = {}) {
super(message);
this.name = this.constructor.name;
this.code = code;
this.details = details;
this.userMessage = userMessage || 'Something went wrong';
}
toJSON() {
return { name: this.name, message: this.message, code: this.code, details: this.details };
}
}
// Error logging service (production)
const ErrorLogger = {
log(error, context = {}) {
const payload = {
name: error.name,
message: error.message,
stack: error.stack,
url: window.location.href,
timestamp: new Date().toISOString(),
...context
};
console.error('[ErrorLogger]', payload);
// In production: fetch('/api/errors', { method: 'POST', body: JSON.stringify(payload) });
}
};
// Centralized error handler
function handleError(error) {
ErrorLogger.log(error);
if (error instanceof ValidationError) {
showFieldError(error.field, error.userMessage);
} else if (error instanceof NetworkError) {
showRetryDialog(error.userMessage);
} else if (error instanceof AuthError) {
redirectToLogin();
} else {
showGenericError(error.userMessage);
}
}
// Assert helper
function assert(condition, message) {
if (!condition) throw new AppError(message, { code: 'ASSERTION_FAILED' });
}
Lo kar liya — Key Points:
- ✅ Always throw Error objects (not strings) — they carry stack traces essential for debugging
- ✅ Create custom error classes extending Error — set this.name in constructor for instanceof checks
- ✅ Use instanceof to catch specific error types and handle them differently (ValidationError vs NetworkError vs AuthError)
- ✅ finally always runs regardless of try/catch outcome — use for cleanup, never for returns
- ✅ .then(f).catch(handler) catches errors from both the promise AND the handler; .then(f, handler) only catches the promise
- ✅ Never leave catch blocks empty — at minimum console.error(err), or re-throw if you can't handle it
- ✅ window.onerror catches sync errors; unhandledrejection catches unhandled promise rejections — both are safety nets
- ✅ In production, log errors to a monitoring service but never expose stack traces to users
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login