Understanding Errors
Errors are inevitable in programming. Proper error handling makes code more robust and easier to debug.
🚨 Types of Errors
SyntaxError
// Missing closing parenthesis
// console.log('Hello'; // SyntaxError
// Invalid syntax
// let 123name = 'John'; // SyntaxError
// Missing comma
// let obj = { a: 1 b: 2 }; // SyntaxError
// Caught at parse time (before execution)
// Cannot be caught with try-catch
ReferenceError
// Using undeclared variable
console.log(nonExistent); // ReferenceError: nonExistent is not defined
// Accessing before declaration (TDZ)
console.log(x); // ReferenceError
let x = 5;
// Misspelled variable
let userName = 'John';
console.log(username); // ReferenceError (case sensitive)
TypeError
// Calling non-function
let num = 42;
num(); // TypeError: num is not a function
// Accessing property of undefined/null
let obj = null;
console.log(obj.name); // TypeError: Cannot read property 'name' of null
// Reassigning const
const x = 5;
x = 10; // TypeError: Assignment to constant variable
// Wrong argument type
let str = 'hello';
str.toFixed(2); // TypeError: str.toFixed is not a function
RangeError
// Array length out of range
let arr = new Array(-1); // RangeError: Invalid array length
// Number out of range
let num = (999).toFixed(200); // RangeError: toFixed() digits argument must be between 0 and 100
// Maximum call stack (infinite recursion)
function infinite() {
infinite();
}
infinite(); // RangeError: Maximum call stack size exceeded
Other Error Types
// URIError - invalid URI functions
decodeURIComponent('%'); // URIError
// EvalError - eval() errors (rarely used)
// Custom errors (covered later)
throw new Error('Custom error message');
🛡️ try...catch...finally
Basic try-catch
// Catch and handle errors
try {
let result = riskyOperation();
console.log(result);
} catch (error) {
console.error('Error occurred:', error.message);
}
// Example: parsing JSON
try {
let data = JSON.parse('invalid json');
} catch (error) {
console.error('Failed to parse:', error);
// Provide fallback
let data = { default: true };
}
// Code continues after catch
console.log('Program continues');
Error Object Properties
try {
nonExistent.property;
} catch (error) {
console.log(error.name); // 'ReferenceError'
console.log(error.message); // 'nonExistent is not defined'
console.log(error.stack); // Stack trace string
console.log(error.toString()); // 'ReferenceError: nonExistent is not defined'
}
// Conditional error handling
try {
riskyOperation();
} catch (error) {
if (error instanceof TypeError) {
console.log('Type error:', error.message);
} else if (error instanceof ReferenceError) {
console.log('Reference error:', error.message);
} else {
console.log('Unknown error:', error);
}
}
finally Block
// finally always runs (success or error)
try {
console.log('Trying...');
throw new Error('Oops');
} catch (error) {
console.log('Caught:', error.message);
} finally {
console.log('Finally runs always');
}
// Practical: cleanup
let file;
try {
file = openFile('data.txt');
processFile(file);
} catch (error) {
console.error('Error processing file:', error);
} finally {
if (file) {
closeFile(file); // Always close file
}
}
// finally runs even with return
function test() {
try {
return 'try';
} catch (error) {
return 'catch';
} finally {
console.log('Finally runs');
// Note: return in finally overrides try/catch return
}
}
test(); // Logs "Finally runs", returns "try"
Nested try-catch
try {
try {
throw new Error('Inner error');
} catch (error) {
console.log('Inner catch:', error.message);
throw new Error('Outer error'); // Re-throw or throw new
}
} catch (error) {
console.log('Outer catch:', error.message);
}
// Practical: try operations in sequence
function processData(data) {
try {
let parsed = JSON.parse(data);
try {
validate(parsed);
return transform(parsed);
} catch (error) {
console.error('Validation error:', error);
return getDefaultData();
}
} catch (error) {
console.error('Parse error:', error);
return null;
}
}
🎯 Throwing Errors
throw Statement
// Throw string (not recommended)
throw 'Error message';
// Throw number
throw 404;
// Throw object
throw { code: 404, message: 'Not found' };
// Throw Error (best practice)
throw new Error('Something went wrong');
// Different error types
throw new TypeError('Invalid type');
throw new ReferenceError('Variable not found');
throw new RangeError('Out of range');
Custom Errors
// ES6 class extending Error
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
// Usage
function validateEmail(email) {
if (!email.includes('@')) {
throw new ValidationError('Invalid email format', 'email');
}
}
try {
validateEmail('notanemail');
} catch (error) {
if (error instanceof ValidationError) {
console.log(`Validation failed for ${error.field}: ${error.message}`);
} else {
console.log('Other error:', error);
}
}
// More custom errors
class NotFoundError extends Error {
constructor(resource) {
super(`${resource} not found`);
this.name = 'NotFoundError';
this.statusCode = 404;
}
}
class AuthenticationError extends Error {
constructor(message = 'Authentication failed') {
super(message);
this.name = 'AuthenticationError';
this.statusCode = 401;
}
}
// Using custom errors
function findUser(id) {
let user = database.find(id);
if (!user) {
throw new NotFoundError('User');
}
return user;
}
function requireAuth(user) {
if (!user.isAuthenticated) {
throw new AuthenticationError();
}
}
When to Throw Errors
// Validate function arguments
function divide(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('Arguments must be numbers');
}
if (b === 0) {
throw new RangeError('Cannot divide by zero');
}
return a / b;
}
// Check preconditions
function processUser(user) {
if (!user) {
throw new Error('User is required');
}
if (!user.id) {
throw new ValidationError('User ID is required', 'id');
}
// Process user
}
// Indicate impossible states
function handleStatus(status) {
switch (status) {
case 'pending':
return 'Processing...';
case 'complete':
return 'Done';
case 'error':
return 'Failed';
default:
throw new Error(`Unknown status: ${status}`);
}
}
🔍 Debugging Techniques
console Methods
// Basic logging
console.log('Message', variable);
// Multiple values
console.log('User:', user, 'Age:', age);
// Formatted strings
console.log('User %s is %d years old', name, age);
// Error level (shows red in console)
console.error('Error occurred:', error);
// Warning level (shows yellow)
console.warn('Deprecated function used');
// Info level
console.info('Application started');
// Debug level (may be filtered)
console.debug('Debug data:', data);
// Table format (great for arrays/objects)
let users = [
{ id: 1, name: 'John', age: 30 },
{ id: 2, name: 'Jane', age: 25 }
];
console.table(users);
// Group related logs
console.group('User Details');
console.log('Name:', user.name);
console.log('Age:', user.age);
console.groupEnd();
// Collapsed group
console.groupCollapsed('Advanced Info');
console.log('Detail 1');
console.log('Detail 2');
console.groupEnd();
// Time operations
console.time('Operation');
expensiveOperation();
console.timeEnd('Operation'); // Logs elapsed time
// Count occurrences
function onClick() {
console.count('Clicked'); // Clicked: 1, Clicked: 2, etc.
}
// Assert (logs only if false)
console.assert(user.age >= 18, 'User must be adult');
// Stack trace
console.trace('Call stack');
Debugger Statement
function complexFunction(data) {
let processed = preProcess(data);
debugger; // Execution pauses here if DevTools open
let result = mainProcess(processed);
return result;
}
// Conditional debugging
function debug(value, threshold) {
if (value > threshold) {
debugger; // Only pause if condition met
}
}
Error Boundaries Pattern
// Wrap risky code
function safeExecute(fn, fallback) {
try {
return fn();
} catch (error) {
console.error('Error in safe execute:', error);
return fallback !== undefined ? fallback : null;
}
}
// Usage
let result = safeExecute(() => {
return riskyOperation();
}, 'default value');
// Multiple operations
function executeAll(...functions) {
let results = [];
let errors = [];
functions.forEach((fn, index) => {
try {
results.push(fn());
} catch (error) {
errors.push({ index, error });
results.push(null);
}
});
if (errors.length > 0) {
console.error('Some operations failed:', errors);
}
return results;
}
// Usage
let [result1, result2, result3] = executeAll(
() => operation1(),
() => operation2(),
() => operation3()
);
🎨 Best Practices
// 1. Fail fast - validate early
function processOrder(order) {
if (!order) throw new Error('Order required');
if (!order.id) throw new Error('Order ID required');
if (order.total < 0) throw new RangeError('Invalid total');
// Process order...
}
// 2. Specific error messages
throw new Error('User not found'); // Good
throw new Error('Error'); // Bad
// 3. Use appropriate error types
throw new TypeError('Expected string'); // For type errors
throw new RangeError('Value out of range'); // For range errors
// 4. Don't swallow errors silently
try {
riskyOperation();
} catch (error) {
// Bad: empty catch
}
try {
riskyOperation();
} catch (error) {
// Good: at least log it
console.error('Operation failed:', error);
}
// 5. Provide context
function loadConfig(filename) {
try {
return JSON.parse(fs.readFileSync(filename));
} catch (error) {
throw new Error(`Failed to load config from ${filename}: ${error.message}`);
}
}
// 6. Don't use exceptions for control flow
// Bad
try {
for (let i = 0; ; i++) {
console.log(array[i]);
}
} catch (error) {
// End of array
}
// Good
for (let i = 0; i < array.length; i++) {
console.log(array[i]);
}
// 7. Document thrown errors
/**
* Divides two numbers
* @param {number} a - Dividend
* @param {number} b - Divisor
* @returns {number} Result
* @throws {TypeError} If arguments are not numbers
* @throws {RangeError} If b is zero
*/
function divide(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('Arguments must be numbers');
}
if (b === 0) {
throw new RangeError('Cannot divide by zero');
}
return a / b;
}
💡 Practical Examples
Input Validation
function validateUserInput(data) {
let errors = [];
if (!data.email || !data.email.includes('@')) {
errors.push('Invalid email');
}
if (!data.password || data.password.length < 8) {
errors.push('Password must be 8+ characters');
}
if (!data.age || data.age < 18) {
errors.push('Must be 18 or older');
}
if (errors.length > 0) {
throw new ValidationError(errors.join(', '));
}
return true;
}
// Usage
try {
validateUserInput(formData);
submitForm(formData);
} catch (error) {
if (error instanceof ValidationError) {
displayErrors(error.message);
} else {
console.error('Unexpected error:', error);
}
}
API Error Handling
async function fetchUser(id) {
try {
let response = await fetch(`/api/users/${id}`);
if (!response.ok) {
if (response.status === 404) {
throw new NotFoundError('User');
} else if (response.status === 401) {
throw new AuthenticationError();
} else {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
}
return await response.json();
} catch (error) {
if (error instanceof NotFoundError) {
console.error('User not found');
return null;
} else if (error instanceof AuthenticationError) {
console.error('Not authenticated');
redirectToLogin();
} else {
console.error('Failed to fetch user:', error);
throw error; // Re-throw unexpected errors
}
}
}
Retry with Error Handling
async function retryOperation(fn, maxRetries = 3, delay = 1000) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
console.log(`Attempt ${attempt} failed:`, error.message);
if (attempt === maxRetries) {
throw new Error(`Failed after ${maxRetries} attempts: ${error.message}`);
}
// Wait before retry
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Usage
try {
let result = await retryOperation(
() => fetch('/api/data').then(r => r.json()),
3,
1000
);
console.log('Success:', result);
} catch (error) {
console.error('All retries failed:', error);
showErrorMessage('Unable to load data');
}
Global Error Handler
// Catch unhandled errors
window.addEventListener('error', event => {
console.error('Uncaught error:', event.error);
// Log to error tracking service
logError({
message: event.error.message,
stack: event.error.stack,
url: window.location.href,
userAgent: navigator.userAgent
});
// Show user-friendly message
showNotification('An error occurred. Please try again.');
// Prevent default browser error handling
event.preventDefault();
});
// Catch unhandled promise rejections
window.addEventListener('unhandledrejection', event => {
console.error('Unhandled promise rejection:', event.reason);
logError({
type: 'unhandledRejection',
reason: event.reason
});
event.preventDefault();
});
Graceful Degradation
function initializeApp() {
try {
// Try to use modern feature
if ('IntersectionObserver' in window) {
initLazyLoading();
} else {
// Fallback for old browsers
loadAllImages();
}
} catch (error) {
console.error('Failed to initialize:', error);
// Use basic functionality
initBasicMode();
}
try {
// Try to load user preferences
let prefs = JSON.parse(localStorage.getItem('prefs'));
applyPreferences(prefs);
} catch (error) {
console.warn('Could not load preferences:', error);
// Use defaults
applyPreferences(getDefaultPreferences());
}
try {
// Try to enable analytics
initAnalytics();
} catch (error) {
// Non-critical, just log
console.warn('Analytics unavailable:', error);
}
}
🎯 Key Takeaways
- try-catch: Wrap risky code to handle errors gracefully
- Error Types: SyntaxError, ReferenceError, TypeError, RangeError
- throw: Create errors with meaningful messages, use Error objects
- finally: Always runs, perfect for cleanup operations
- Custom Errors: Extend Error class for specific error types
- console.error: Log errors properly, don't swallow them silently
- debugger: Pause execution in DevTools for inspection
- Fail Fast: Validate early, throw specific errors with context