Types of Errors in JavaScript

Understanding and Handling JavaScript's Error System

Introduction to JavaScript Errors

JavaScript errors are signals that something unexpected has happened during code execution. Understanding these errors is crucial for writing robust, bug-free applications.

Errors in JavaScript typically fall into three categories:

graph TD A[JavaScript Errors] --> B[Syntax Errors] A --> C[Runtime Errors] A --> D[Logical Errors] B --> B1[Parse-time errors that prevent execution] C --> C1[Execution-time errors that throw exceptions] D --> D1[Code executes but produces incorrect results] style A fill:#f5f5f5,stroke:#333 style B fill:#ffcdd2,stroke:#c62828 style C fill:#fff9c4,stroke:#f9a825 style D fill:#e1f5fe,stroke:#0288d1

JavaScript has a built-in Error object hierarchy that provides standardized ways to represent and handle these errors during execution.

The Error Object Hierarchy

JavaScript includes several built-in error types that inherit from the base Error constructor. Each represents a specific category of error.

Error SyntaxError ReferenceError TypeError RangeError URIError EvalError AggregateError Common Error Properties • name: Type of error • message: Description of error • stack: Stack trace (non-standard)

Each error type serves a specific purpose and is thrown in different situations. Let's explore each one in detail.

Syntax Errors

Syntax errors (also known as parsing errors) occur when the JavaScript engine tries to parse code that doesn't follow the language's syntax rules. These errors prevent your code from executing at all.

// Missing closing parenthesis
function add(a, b {
    return a + b;
}
// SyntaxError: Unexpected token '{'

// Invalid variable name
const 123variable = "hello";
// SyntaxError: Invalid or unexpected token

// Missing quotes in string
const name = John;
// SyntaxError: Unexpected identifier 'John'

// Extra comma in array
const arr = [1, 2, 3, ];
// No error in modern JavaScript (trailing commas are allowed)

// Invalid JSON
const jsonData = '{"name": "John", age: 30}';
JSON.parse(jsonData);
// SyntaxError: Unexpected token 'a' in JSON at position 15

Detecting and Fixing Syntax Errors

Syntax errors are usually detected before code execution, making them relatively easy to spot and fix:

Pro Tip: Common Syntax Error Fixes

  • Check for missing or mismatched brackets, parentheses, and quotes
  • Verify that all statements end with semicolons (if that's your coding style)
  • Ensure variable names follow JavaScript naming rules
  • Look for missing commas in object and array literals
  • Validate JSON with a JSON linter before parsing

Reference Errors

Reference errors occur when you try to use a variable or function that hasn't been declared or is out of scope. These are runtime errors that happen during code execution.

// Undefined variable
console.log(undefinedVariable);
// ReferenceError: undefinedVariable is not defined

// Using a variable before declaration in temporal dead zone
console.log(myVar);  // ReferenceError
let myVar = 10;

// Accessing a variable outside its scope
function myFunction() {
    const localVar = "I'm local";
}
console.log(localVar);
// ReferenceError: localVar is not defined

// Common mistake: typos in variable names
const firstName = "John";
console.log(frstName);
// ReferenceError: frstName is not defined

Understanding Temporal Dead Zone

One common source of reference errors is the "temporal dead zone" (TDZ), which affects variables declared with let and const.

// Variables declared with var are hoisted and initialized with undefined
console.log(varVariable);  // undefined (no error)
var varVariable = "I'm a var";

// Let and const are hoisted but not initialized - they're in the "temporal dead zone"
console.log(letVariable);  // ReferenceError
let letVariable = "I'm a let";

// Same for const
console.log(constVariable);  // ReferenceError
const constVariable = "I'm a const";

// The TDZ ends when the variable declaration is encountered
function tdz() {
    // TDZ starts for x
    const tmp = typeof x;  // No error - typeof special behavior with undeclared variables
    console.log(tmp);      // "undefined"
    
    // Still in TDZ
    // console.log(x);     // This would cause a ReferenceError
    
    // TDZ ends for x
    const x = "I exist!";
    console.log(x);        // "I exist!"
}

Pro Tip: Preventing Reference Errors

  • Always declare variables before using them
  • Use linters to catch undeclared variables
  • Be careful with variable scope, especially in closures
  • When checking if a variable might exist, use typeof or window.hasOwnProperty() rather than directly accessing it
  • Use strict mode ('use strict';) to catch accidental global variables

Type Errors

Type errors occur when an operation is performed on a value of the wrong type, or when a value is used in a way that's incompatible with its type.

// Calling something that's not a function
const notAFunction = 42;
notAFunction();
// TypeError: notAFunction is not a function

// Accessing a property on null or undefined
const obj = null;
console.log(obj.property);
// TypeError: Cannot read properties of null (reading 'property')

// Using a method on an incompatible type
const num = 42;
num.toUpperCase();
// TypeError: num.toUpperCase is not a function

// Attempting to modify a const variable
const immutable = "can't change this";
immutable = "trying to change";
// TypeError: Assignment to constant variable

// Using an array method on a non-array
const notArray = "I'm a string";
notArray.forEach(item => console.log(item));
// TypeError: notArray.forEach is not a function

Common Type Error Scenarios

// Optional chaining operator (?.) helps prevent type errors when accessing properties
const user = null;
// console.log(user.name);  // TypeError
console.log(user?.name);    // undefined (no error)

// Nullish coalescing operator (??) helps with null/undefined defaults
const value = null;
const result = value ?? "default";
console.log(result);  // "default"

// Type checking helps prevent errors
function processUser(user) {
    if (typeof user !== 'object' || user === null) {
        throw new TypeError('Expected an object');
    }
    
    return user.name;
}

Pro Tip: Preventing Type Errors

  • Check the type of values before performing operations on them
  • Use optional chaining (?.) for property access that might be null or undefined
  • Provide default values using nullish coalescing (??) or logical OR (||)
  • Validate function arguments at the beginning of functions
  • Consider using TypeScript for static type checking

Range Errors

Range errors occur when a value is outside the range of acceptable values for an operation.

// Invalid array length
const arr = [];
arr.length = -1;
// RangeError: Invalid array length

// Recursion exceeding maximum call stack size
function infiniteRecursion() {
    infiniteRecursion();
}
// infiniteRecursion();
// RangeError: Maximum call stack size exceeded

// Invalid numeric precision
const num = 123;
num.toPrecision(500);
// RangeError: toPrecision() argument must be between 1 and 100

// Invalid Date manipulation
const invalidDate = new Date('2023-02-30');  // Feb 30 doesn't exist
console.log(invalidDate);  // Invalid Date (but no error)

invalidDate.setMonth(100);  // Results might be unexpected but no RangeError in this case

Pro Tip: Preventing Range Errors

  • Validate numeric inputs that control array sizes, precisions, or loop iterations
  • Ensure recursive functions have proper termination conditions
  • When setting properties with restricted ranges (like array length), check the bounds first
  • Validate date inputs and operations to ensure they're within acceptable ranges

URI Errors

URI errors are thrown when the global URI handling functions are used incorrectly. These are relatively rare in everyday programming.

// Malformed URI in decodeURI
decodeURI('%');
// URIError: URI malformed

// Invalid encoding in encodeURI
encodeURI('\uD800');  // Unpaired surrogate
// URIError: URI malformed

// Other URI functions that can throw URIError
try {
    decodeURIComponent('%E0%A4%A');
} catch (e) {
    console.log(e instanceof URIError);  // true
    console.log(e.message);  // "URI malformed"
}

Pro Tip: Safe URI Handling

  • Always wrap URI encoding/decoding functions in try-catch blocks
  • Validate URI strings before processing them
  • Consider using libraries like 'safe-encode-uri-component' for more robust handling

Other Error Types

Eval Error

EvalError is thrown when there's an error in the eval() function. In modern JavaScript, this error is rarely thrown directly, as most eval errors now throw SyntaxError or other more specific error types.

// Historical example (might not throw EvalError in modern browsers)
// eval('throw new EvalError("Eval error example")');

// Modern browsers typically throw SyntaxError for eval errors
try {
    eval('if (true) {');  // Incomplete if statement
} catch (e) {
    console.log(e instanceof SyntaxError);  // true
    console.log(e.message);  // "Unexpected end of input"
}

Aggregate Error

AggregateError is a newer error type introduced in ES2020. It's used to represent multiple errors as a single error, particularly in operations like Promise.any().

// Example using Promise.any() (ES2021)
const promises = [
    Promise.reject(new Error("Failure 1")),
    Promise.reject(new Error("Failure 2")),
    Promise.reject(new Error("Failure 3"))
];

Promise.any(promises).catch(errors => {
    console.log(errors instanceof AggregateError);  // true
    console.log(errors.message);  // "All promises were rejected"
    console.log(errors.errors);   // [Error: Failure 1, Error: Failure 2, Error: Failure 3]
});

// Creating a custom AggregateError
try {
    throw new AggregateError([
        new Error('Error 1'),
        new Error('Error 2')
    ], 'Multiple errors occurred');
} catch (e) {
    console.log(e.message);  // "Multiple errors occurred"
    console.log(e.errors);   // [Error: Error 1, Error: Error 2]
}

Internal Error

InternalError is a non-standard error type specific to some JavaScript engines, representing errors that occur internally in the engine, rather than in the user's code.

// InternalError examples (browser-specific, might not work everywhere)
// These are examples of what might cause InternalError in some engines

// 1. Too many switch cases
// function tooManySwitchCases() {
//     switch(1) {
//         case 1: break; case 2: break; /* ... thousands more cases ... */
//     }
// }

// 2. Too much recursion (often throws RangeError instead)
// function tooMuchRecursion() {
//     return tooMuchRecursion();
// }

// In practice, you'd rarely encounter or need to catch InternalError specifically

Custom Error Types

JavaScript allows you to create your own error types by extending the built-in Error class or its subclasses. This is useful for creating domain-specific errors in your applications.

// Basic custom error
class ValidationError extends Error {
    constructor(message) {
        super(message);
        this.name = "ValidationError";
    }
}

// More elaborate custom error with additional properties
class APIError extends Error {
    constructor(message, statusCode, endpoint) {
        super(message);
        this.name = "APIError";
        this.statusCode = statusCode;
        this.endpoint = endpoint;
        this.timestamp = new Date();
        
        // Capture stack trace (works in V8 environments like Chrome and Node.js)
        if (Error.captureStackTrace) {
            Error.captureStackTrace(this, APIError);
        }
    }
    
    toJSON() {
        return {
            name: this.name,
            message: this.message,
            statusCode: this.statusCode,
            endpoint: this.endpoint,
            timestamp: this.timestamp
        };
    }
}

// Using custom errors
function validateUsername(username) {
    if (!username) {
        throw new ValidationError("Username is required");
    }
    
    if (username.length < 3) {
        throw new ValidationError("Username must be at least 3 characters");
    }
    
    return username;
}

function fetchData(endpoint) {
    if (endpoint === '/users') {
        return ['user1', 'user2'];
    }
    
    throw new APIError("Endpoint not found", 404, endpoint);
}

// Handling custom errors
try {
    validateUsername("ab");
} catch (error) {
    if (error instanceof ValidationError) {
        console.log(`Validation error: ${error.message}`);
    } else {
        console.log(`Unexpected error: ${error.message}`);
    }
}

try {
    fetchData('/invalid');
} catch (error) {
    if (error instanceof APIError) {
        console.log(`API error (${error.statusCode}): ${error.message}`);
        console.log(`Endpoint: ${error.endpoint}`);
    } else {
        console.log(`Unexpected error: ${error.message}`);
    }
}

Error Hierarchies

For complex applications, you can create an entire hierarchy of custom error types:

// Base application error
class AppError extends Error {
    constructor(message) {
        super(message);
        this.name = this.constructor.name;
        
        if (Error.captureStackTrace) {
            Error.captureStackTrace(this, this.constructor);
        }
    }
}

// Domain-specific errors
class DatabaseError extends AppError {}
class NetworkError extends AppError {}
class SecurityError extends AppError {}

// More specific errors
class ConnectionError extends DatabaseError {
    constructor(message, connectionDetails) {
        super(message);
        this.connectionDetails = connectionDetails;
    }
}

class AuthenticationError extends SecurityError {
    constructor(message, username) {
        super(message);
        this.username = username;
        this.timestamp = new Date();
    }
}

// Using the error hierarchy
try {
    throw new AuthenticationError("Invalid password", "user123");
} catch (error) {
    if (error instanceof SecurityError) {
        console.log("Security issue:", error.message);
        
        if (error instanceof AuthenticationError) {
            console.log(`User: ${error.username}`);
            console.log(`Time: ${error.timestamp}`);
        }
    } else if (error instanceof DatabaseError) {
        console.log("Database issue:", error.message);
    } else if (error instanceof AppError) {
        console.log("Application error:", error.message);
    } else {
        console.log("Unknown error:", error);
    }
}

Pro Tip: Custom Error Best Practices

  • Always extend from Error or a subclass for proper inheritance
  • Set a descriptive name property to identify the error type
  • Include relevant contextual information as properties
  • Use Error.captureStackTrace() in environments that support it
  • Consider adding a toJSON() method for serialization
  • Create a logical hierarchy of error types for complex applications

Logical Errors

Logical errors are arguably the most challenging kind of errors to detect and fix. These occur when your code runs without throwing any exceptions but doesn't behave as expected.

// Off-by-one error
function sumFirstN(n) {
    let sum = 0;
    for (let i = 0; i < n; i++) {
        sum += i;  // This doesn't include n itself
    }
    return sum;
}
console.log(sumFirstN(5));  // 10 (0+1+2+3+4), but should be 15 (1+2+3+4+5)

// Correct version
function sumFirstNCorrect(n) {
    let sum = 0;
    for (let i = 1; i <= n; i++) {  // Start from 1, include n
        sum += i;
    }
    return sum;
}
console.log(sumFirstNCorrect(5));  // 15

// Comparison error
function isAdult(age) {
    if (age = 18) {  // Assignment instead of comparison (age === 18)
        return true;
    }
    return false;
}
console.log(isAdult(16));  // true (incorrect)

// Incorrect operator precedence
let result = 3 + 5 * 2;  // 13, not 16
console.log(result);

// Forgetting to return a value
function multiply(a, b) {
    a * b;  // Missing return statement
}
console.log(multiply(2, 3));  // undefined

Detecting and Fixing Logical Errors

// Using debug logging to find logical errors
function calculateTotal(items, tax) {
    console.log('Items:', items);
    
    let subtotal = 0;
    for (const item of items) {
        subtotal += item.price * item.quantity;
        console.log(`Added ${item.quantity}x ${item.name}: $${item.price * item.quantity}`);
    }
    console.log('Subtotal:', subtotal);
    
    const taxAmount = subtotal * tax;
    console.log('Tax amount:', taxAmount);
    
    const total = subtotal + taxAmount;
    console.log('Final total:', total);
    
    return total;
}

const items = [
    { name: 'Widget', price: 9.99, quantity: 2 },
    { name: 'Gadget', price: 14.99, quantity: 1 }
];

const total = calculateTotal(items, 0.07);
console.log('Expected total for 2x $9.99 and 1x $14.99 with 7% tax:');
console.log((2 * 9.99 + 1 * 14.99) * 1.07);
console.log('Actual total:', total);

Pro Tip: Preventing Logical Errors

  • Write clear, explicit code that doesn't rely on subtle behavior
  • Use meaningful variable and function names
  • Add comments for complex logic
  • Be careful with operators, especially assignment (=) vs equality (===)
  • Watch out for type coercion in comparisons
  • Always use strict equality (===) instead of loose equality (==) when possible
  • Pay attention to function return values
  • Use code formatting to make structure clear

Network and Asynchronous Errors

Modern JavaScript applications often interact with servers and APIs, introducing another category of errors related to network operations and asynchronous code.

Fetch API and Promise Errors

// Fetch API error handling
fetch('https://api.example.com/data')
    .then(response => {
        // Check if the response is successful
        if (!response.ok) {
            throw new Error(`HTTP error! Status: ${response.status}`);
        }
        return response.json();
    })
    .then(data => {
        console.log('Data received:', data);
    })
    .catch(error => {
        console.error('Fetch error:', error);
    });

// Using async/await for cleaner error handling
async function fetchData() {
    try {
        const response = await fetch('https://api.example.com/data');
        
        if (!response.ok) {
            throw new Error(`HTTP error! Status: ${response.status}`);
        }
        
        const data = await response.json();
        return data;
    } catch (error) {
        console.error('Fetch error:', error);
        // Re-throw or return a default value
        return { error: true, message: error.message };
    }
}

// Error handling with Promise.all
async function fetchMultipleResources() {
    try {
        const [users, products] = await Promise.all([
            fetch('/api/users').then(r => r.json()),
            fetch('/api/products').then(r => r.json())
        ]);
        
        // Process data
        console.log('Users:', users);
        console.log('Products:', products);
    } catch (error) {
        // If any promise rejects, this catch block will execute
        console.error('Failed to fetch all resources:', error);
    }
}

Common Network Error Scenarios

// More comprehensive error handling for API requests
async function apiRequest(url, options = {}) {
    const controller = new AbortController();
    const signal = controller.signal;
    
    // Set timeout (abort after 10 seconds)
    const timeout = setTimeout(() => controller.abort(), 10000);
    
    try {
        const response = await fetch(url, { 
            ...options, 
            signal,
            headers: {
                'Content-Type': 'application/json',
                ...options.headers
            }
        });
        
        // Clear timeout
        clearTimeout(timeout);
        
        // Handle HTTP errors
        if (!response.ok) {
            // Try to get error details from response
            let errorMessage;
            try {
                const errorData = await response.json();
                errorMessage = errorData.message || `HTTP error ${response.status}`;
            } catch (e) {
                errorMessage = `HTTP error ${response.status}`;
            }
            
            const error = new Error(errorMessage);
            error.status = response.status;
            error.statusText = response.statusText;
            throw error;
        }
        
        // Parse JSON (might throw if response is invalid)
        const data = await response.json();
        return data;
    } catch (error) {
        // Clear timeout
        clearTimeout(timeout);
        
        // Handle specific error types
        if (error.name === 'AbortError') {
            throw new Error('Request timeout');
        }
        
        if (error instanceof SyntaxError) {
            throw new Error('Invalid response format');
        }
        
        // Re-throw the error with additional context
        error.url = url;
        throw error;
    }
}

Pro Tip: Handling Network and Async Errors

  • Always use try/catch with async/await code
  • Implement timeouts for network requests
  • Check response.ok before processing responses
  • Add fallbacks or retry mechanisms for critical operations
  • Provide clear error messages to help users understand what went wrong
  • Consider using libraries like Axios that have better error handling than fetch

Browser-Specific Error Handling

Working with the DOM and browser APIs introduces additional error types and handling considerations.

DOM Errors

// Element not found errors
try {
    const element = document.querySelector('#non-existent');
    element.innerHTML = 'This will fail';  // TypeError: Cannot set property 'innerHTML' of null
} catch (error) {
    console.error('DOM error:', error.message);
}

// Safer DOM manipulation
const element = document.querySelector('#maybe-exists');
if (element) {
    element.innerHTML = 'This is safe';
} else {
    console.log('Element not found');
}

// DOMException errors
try {
    document.domain = 'example.com';  // Could throw DOMException in certain conditions
} catch (error) {
    if (error instanceof DOMException) {
        console.error(`DOM Exception (${error.code}): ${error.message}`);
    } else {
        console.error('Other error:', error);
    }
}

// Event listener errors
document.getElementById('my-button')?.addEventListener('click', function() {
    try {
        // Code that might throw an error during event handling
        doSomethingRisky();
    } catch (error) {
        console.error('Event handler error:', error);
        // Don't rethrow - would stop other event handlers
    }
});

Uncaught Errors in Browsers

Browser environments provide global error handlers for catching otherwise uncaught errors:

// Global error handler
window.onerror = function(message, source, lineno, colno, error) {
    console.error('Global error handler:');
    console.error('Message:', message);
    console.error('Source:', source);
    console.error('Line:', lineno);
    console.error('Column:', colno);
    console.error('Error object:', error);
    
    // Return true to prevent the browser's default error handler
    return true;
};

// For promise rejection errors
window.onunhandledrejection = function(event) {
    console.error('Unhandled promise rejection:');
    console.error('Reason:', event.reason);
    
    // Prevent default handling
    event.preventDefault();
};

Browser Developer Tools

Modern browsers provide excellent tools for debugging JavaScript errors:

Error Handling Strategies

Let's explore different approaches to handling errors in JavaScript applications.

1. Preventive Error Handling

The best way to handle errors is to prevent them from occurring in the first place.

// Validate inputs to prevent errors
function divide(a, b) {
    // Type checking
    if (typeof a !== 'number' || typeof b !== 'number') {
        throw new TypeError('Both arguments must be numbers');
    }
    
    // Value validation
    if (b === 0) {
        throw new Error('Cannot divide by zero');
    }
    
    return a / b;
}

// Use default values to prevent undefined/null errors
function greet(name = 'Guest', greeting = 'Hello') {
    return `${greeting}, ${name}!`;
}

// Object property access with optional chaining
function getUserCity(user) {
    return user?.address?.city || 'Unknown';
}

// Type coercion to ensure consistent types
function concatenateValues(a, b) {
    return String(a) + String(b);
}

2. Try/Catch Blocks

Use try/catch blocks to handle expected errors gracefully.

// Basic try/catch
try {
    // Code that might throw an error
    const data = JSON.parse(jsonString);
    processData(data);
} catch (error) {
    // Handle the error
    console.error('Failed to process JSON:', error.message);
}

// Try/catch with finally
function processFile(filename) {
    let file = null;
    
    try {
        file = openFile(filename);
        return processFileContents(file);
    } catch (error) {
        console.error(`Error processing ${filename}:`, error);
        return null;
    } finally {
        // This code always runs, even if there's an error or return
        if (file) {
            closeFile(file);
        }
    }
}

// Conditional catch blocks (in modern JavaScript)
try {
    // Risky code
    doSomething();
} catch (error) {
    if (error instanceof TypeError) {
        handleTypeError(error);
    } else if (error instanceof RangeError) {
        handleRangeError(error);
    } else {
        console.error('Unexpected error:', error);
        throw error;  // Re-throw unexpected errors
    }
}

3. Promises and Async/Await Error Handling

// Promise error handling
promiseFunction()
    .then(result => {
        // Process successful result
    })
    .catch(error => {
        // Handle any errors in the promise chain
    })
    .finally(() => {
        // Cleanup code (always runs)
    });

// Async/await with try/catch
async function processData() {
    try {
        const result = await promiseFunction();
        // Process result
        return transformData(result);
    } catch (error) {
        // Handle errors
        logError(error);
        return defaultData;
    } finally {
        // Cleanup code
        cleanupResources();
    }
}

// Error propagation pattern
async function fetchUserData(userId) {
    try {
        return await apiClient.getUser(userId);
    } catch (error) {
        // Add context to the error
        error.userId = userId;
        error.context = 'fetchUserData';
        throw error;  // Re-throw with added context
    }
}

4. Error Boundaries

In component-based architectures (like React), error boundaries can catch errors in specific parts of the UI.

// React Error Boundary example (concept only)
class ErrorBoundary extends React.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 error reporting service
        console.error('Error caught by boundary:', error, errorInfo);
    }
    
    render() {
        if (this.state.hasError) {
            // Render fallback UI
            return this.props.fallback || `<div>Something went wrong.</div>`;
        }
        
        // Render normal children
        return this.props.children;
    }
}

// Usage
// <ErrorBoundary fallback={<ErrorPage />}>
//     <MyComponent />
// </ErrorBoundary>

5. Error Logging and Monitoring

In production applications, it's important to log errors and monitor them.

// Simple error logging service
const errorLogger = {
    logError(error, context = {}) {
        // In production, this would send to a server
        console.error('ERROR LOGGED:', {
            message: error.message,
            name: error.name,
            stack: error.stack,
            context,
            timestamp: new Date().toISOString()
        });
    }
};

// Higher-order function for error logging
function withErrorLogging(fn) {
    return async function(...args) {
        try {
            return await fn(...args);
        } catch (error) {
            errorLogger.logError(error, {
                functionName: fn.name,
                arguments: args
            });
            throw error;  // Re-throw after logging
        }
    };
}

// Using the error logging HOF
const fetchUserWithLogging = withErrorLogging(fetchUser);

// Usage
try {
    const user = await fetchUserWithLogging(123);
} catch (error) {
    // Handle error at the UI level
    showErrorNotification('Failed to load user');
}

Best Practices for Error Handling

  1. Fail Fast: Detect and report errors as early as possible
  2. Be Specific: Use specific error types to indicate what went wrong
  3. Provide Context: Include relevant information in error messages and properties
  4. Handle Errors at the Right Level: Not every function needs to catch all errors
  5. Don't Swallow Errors: If you catch an error, do something meaningful with it
  6. Centralize Error Handling: Have consistent strategies across your application
  7. Log Errors: Capture information for debugging and monitoring
  8. Graceful Degradation: Provide fallbacks when operations fail
  9. Clean Up Resources: Use finally blocks to ensure cleanup happens regardless of errors
  10. Test Error Cases: Write tests specifically for error conditions
// Comprehensive error handling example
class UserService {
    constructor(apiClient, logger) {
        this.api = apiClient;
        this.logger = logger;
        
        // Bind methods and add error logging
        this.getUser = this.wrapWithErrorHandling(this.getUser);
        this.updateUser = this.wrapWithErrorHandling(this.updateUser);
    }
    
    // Higher-order function for consistent error handling
    wrapWithErrorHandling(method) {
        return async (...args) => {
            try {
                return await method.apply(this, args);
            } catch (error) {
                // Add context to the error
                if (error.name === 'ApiError') {
                    // Already handled by the API client
                    this.logger.warn(`API error in ${method.name}:`, error);
                } else {
                    // Unexpected error
                    this.logger.error(`Unexpected error in ${method.name}:`, error);
                }
                
                // Convert to application-specific error
                throw new UserServiceError(
                    `Failed to ${method.name}: ${error.message}`,
                    { cause: error, method: method.name, args }
                );
            }
        };
    }
    
    // Service methods
    async getUser(userId) {
        // Validate input
        if (!userId) {
            throw new ValidationError('User ID is required');
        }
        
        // Call API (might throw ApiError)
        const user = await this.api.get(`/users/${userId}`);
        
        // Transform and return data
        return this.transformUserData(user);
    }
    
    async updateUser(userId, data) {
        // Implementation...
    }
    
    // Helper method
    transformUserData(user) {
        // Implementation...
    }
}

// Custom error types
class UserServiceError extends Error {
    constructor(message, details = {}) {
        super(message);
        this.name = 'UserServiceError';
        this.details = details;
        
        if (Error.captureStackTrace) {
            Error.captureStackTrace(this, UserServiceError);
        }
    }
}

class ValidationError extends Error {
    constructor(message) {
        super(message);
        this.name = 'ValidationError';
        
        if (Error.captureStackTrace) {
            Error.captureStackTrace(this, ValidationError);
        }
    }
}

// Usage in a UI component
async function displayUserProfile(userId) {
    const userService = new UserService(apiClient, logger);
    
    try {
        const user = await userService.getUser(userId);
        renderUserProfile(user);
    } catch (error) {
        if (error instanceof ValidationError) {
            showValidationError(error.message);
        } else if (error instanceof UserServiceError) {
            showErrorMessage('Could not load user profile', error.message);
        } else {
            // Unexpected error
            showErrorMessage('An unexpected error occurred');
            logger.error('Unhandled error in displayUserProfile:', error);
        }
    }
}

Practice Activities

Activity 1: Error Type Identification

Review a set of code snippets and identify what types of errors might occur in each. For each potential error, explain how to prevent or handle it properly.

Activity 2: Custom Error Hierarchy

Design and implement a custom error hierarchy for a specific domain, such as a banking application or a game. Create appropriate error types and demonstrate how to use them.

Activity 3: Robust API Client

Build a robust API client wrapper with comprehensive error handling. It should handle network errors, timeout errors, authentication errors, and validation errors appropriately.

Summary

In this lecture, we've explored:

Understanding errors and implementing proper error handling is a critical skill for JavaScript developers. By recognizing the different types of errors, knowing how to prevent them, and implementing appropriate error handling strategies, you can build more robust, maintainable, and user-friendly applications.