javascript

exercises

exercises.js
/**
 * Advanced Error Handling - Exercises
 * Practice error handling patterns in async code
 */

// =============================================================================
// EXERCISE 1: Custom Error Classes
// Create custom error classes for an e-commerce application
// =============================================================================

/*
 * TODO: Create these custom error classes:
 * 1. ProductNotFoundError - with productId and searched category
 * 2. InsufficientStockError - with productId, requested quantity, available quantity
 * 3. PaymentError - with errorCode, transactionId, and amount
 * 4. AuthenticationError - with reason and attempted action
 *
 * Each should:
 * - Extend Error
 * - Set proper name
 * - Include toJSON() method
 */

// YOUR CODE HERE:

// SOLUTION:
/*
class ProductNotFoundError extends Error {
    constructor(productId, category = null) {
        super(`Product ${productId} not found${category ? ` in ${category}` : ''}`);
        this.name = 'ProductNotFoundError';
        this.productId = productId;
        this.category = category;
    }
    
    toJSON() {
        return {
            name: this.name,
            message: this.message,
            productId: this.productId,
            category: this.category
        };
    }
}

class InsufficientStockError extends Error {
    constructor(productId, requested, available) {
        super(`Insufficient stock for ${productId}: requested ${requested}, available ${available}`);
        this.name = 'InsufficientStockError';
        this.productId = productId;
        this.requested = requested;
        this.available = available;
    }
    
    toJSON() {
        return {
            name: this.name,
            message: this.message,
            productId: this.productId,
            requested: this.requested,
            available: this.available
        };
    }
}

class PaymentError extends Error {
    constructor(errorCode, transactionId, amount) {
        super(`Payment failed: ${errorCode}`);
        this.name = 'PaymentError';
        this.errorCode = errorCode;
        this.transactionId = transactionId;
        this.amount = amount;
    }
    
    toJSON() {
        return {
            name: this.name,
            message: this.message,
            errorCode: this.errorCode,
            transactionId: this.transactionId,
            amount: this.amount
        };
    }
}

class AuthenticationError extends Error {
    constructor(reason, attemptedAction) {
        super(`Authentication failed: ${reason}`);
        this.name = 'AuthenticationError';
        this.reason = reason;
        this.attemptedAction = attemptedAction;
    }
    
    toJSON() {
        return {
            name: this.name,
            message: this.message,
            reason: this.reason,
            attemptedAction: this.attemptedAction
        };
    }
}
*/

// =============================================================================
// EXERCISE 2: Error Handler with Type Checking
// Create a function that handles errors differently based on type
// =============================================================================

/*
 * TODO: Create handleOrderError(error) that:
 * - For ProductNotFoundError: return { action: 'remove_item', productId }
 * - For InsufficientStockError: return { action: 'adjust_quantity', max: available }
 * - For PaymentError: return { action: 'retry_payment', waitMs: 1000 }
 * - For AuthenticationError: return { action: 'redirect_login' }
 * - For other errors: return { action: 'contact_support', error: error.message }
 */

function handleOrderError(error) {
  // YOUR CODE HERE:
}

// SOLUTION:
/*
function handleOrderError(error) {
    if (error instanceof ProductNotFoundError) {
        return { action: 'remove_item', productId: error.productId };
    }
    
    if (error instanceof InsufficientStockError) {
        return { action: 'adjust_quantity', max: error.available };
    }
    
    if (error instanceof PaymentError) {
        return { action: 'retry_payment', waitMs: 1000 };
    }
    
    if (error instanceof AuthenticationError) {
        return { action: 'redirect_login' };
    }
    
    return { action: 'contact_support', error: error.message };
}
*/

// =============================================================================
// EXERCISE 3: Retry with Exponential Backoff
// Implement a robust retry mechanism
// =============================================================================

/*
 * TODO: Create retryWithBackoff(fn, options) that:
 * - options: { maxRetries, initialDelay, maxDelay, factor, shouldRetry }
 * - Retries the function with exponential backoff
 * - Respects maxDelay cap
 * - Uses shouldRetry(error) to determine if error is retryable
 * - Returns result on success or throws after all retries exhausted
 * - Logs each attempt with delay information
 */

async function retryWithBackoff(fn, options = {}) {
  // YOUR CODE HERE:
}

// SOLUTION:
/*
async function retryWithBackoff(fn, options = {}) {
    const {
        maxRetries = 3,
        initialDelay = 100,
        maxDelay = 10000,
        factor = 2,
        shouldRetry = () => true
    } = options;
    
    let lastError;
    let delay = initialDelay;
    
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            return await fn();
        } catch (error) {
            lastError = error;
            
            if (!shouldRetry(error) || attempt === maxRetries) {
                throw error;
            }
            
            console.log(`Attempt ${attempt} failed: ${error.message}`);
            console.log(`Retrying in ${delay}ms...`);
            
            await new Promise(resolve => setTimeout(resolve, delay));
            
            delay = Math.min(delay * factor, maxDelay);
        }
    }
    
    throw lastError;
}
*/

// =============================================================================
// EXERCISE 4: Circuit Breaker Implementation
// Build a complete circuit breaker with monitoring
// =============================================================================

/*
 * TODO: Implement CircuitBreaker class:
 * - States: CLOSED (normal), OPEN (failing), HALF_OPEN (testing)
 * - CLOSED → OPEN when failures >= threshold
 * - OPEN → HALF_OPEN after timeout
 * - HALF_OPEN → CLOSED on success, OPEN on failure
 * - Track success/failure counts
 * - Emit events: 'stateChange', 'success', 'failure'
 * - Provide getStats() method
 */

class CircuitBreaker {
  // YOUR CODE HERE:
}

// SOLUTION:
/*
class CircuitBreaker {
    static CLOSED = 'CLOSED';
    static OPEN = 'OPEN';
    static HALF_OPEN = 'HALF_OPEN';
    
    constructor(options = {}) {
        this.threshold = options.threshold || 5;
        this.timeout = options.timeout || 30000;
        this.state = CircuitBreaker.CLOSED;
        this.failures = 0;
        this.successes = 0;
        this.lastFailure = null;
        this.nextAttempt = null;
        this.listeners = {};
    }
    
    on(event, callback) {
        if (!this.listeners[event]) {
            this.listeners[event] = [];
        }
        this.listeners[event].push(callback);
    }
    
    emit(event, data) {
        if (this.listeners[event]) {
            this.listeners[event].forEach(cb => cb(data));
        }
    }
    
    async execute(fn) {
        if (this.state === CircuitBreaker.OPEN) {
            if (Date.now() >= this.nextAttempt) {
                this.transition(CircuitBreaker.HALF_OPEN);
            } else {
                throw new Error('Circuit breaker is OPEN');
            }
        }
        
        try {
            const result = await fn();
            this.onSuccess();
            return result;
        } catch (error) {
            this.onFailure(error);
            throw error;
        }
    }
    
    transition(newState) {
        const oldState = this.state;
        this.state = newState;
        this.emit('stateChange', { from: oldState, to: newState });
        console.log(`Circuit breaker: ${oldState} → ${newState}`);
    }
    
    onSuccess() {
        this.successes++;
        this.failures = 0;
        this.emit('success', { total: this.successes });
        
        if (this.state === CircuitBreaker.HALF_OPEN) {
            this.transition(CircuitBreaker.CLOSED);
        }
    }
    
    onFailure(error) {
        this.failures++;
        this.lastFailure = { error, timestamp: Date.now() };
        this.emit('failure', { error, count: this.failures });
        
        if (this.failures >= this.threshold || this.state === CircuitBreaker.HALF_OPEN) {
            this.transition(CircuitBreaker.OPEN);
            this.nextAttempt = Date.now() + this.timeout;
        }
    }
    
    getStats() {
        return {
            state: this.state,
            failures: this.failures,
            successes: this.successes,
            lastFailure: this.lastFailure,
            nextAttempt: this.nextAttempt
        };
    }
    
    reset() {
        this.state = CircuitBreaker.CLOSED;
        this.failures = 0;
        this.lastFailure = null;
        this.nextAttempt = null;
    }
}
*/

// =============================================================================
// EXERCISE 5: Error Aggregator
// Collect and report multiple errors from parallel operations
// =============================================================================

/*
 * TODO: Create runParallel(tasks, options) that:
 * - Runs all tasks in parallel using Promise.allSettled
 * - options: { failFast, maxErrors, onError }
 * - If failFast: stop on first error (use custom logic)
 * - If maxErrors reached: stop and report
 * - Call onError(error, taskIndex) for each error
 * - Return { results: [...], errors: [...], stats: {...} }
 */

async function runParallel(tasks, options = {}) {
  // YOUR CODE HERE:
}

// SOLUTION:
/*
async function runParallel(tasks, options = {}) {
    const {
        failFast = false,
        maxErrors = Infinity,
        onError = () => {}
    } = options;
    
    const results = [];
    const errors = [];
    let completed = 0;
    let stopped = false;
    
    const wrappedTasks = tasks.map((task, index) => {
        return task()
            .then(result => {
                if (stopped) return { skipped: true };
                completed++;
                return { success: true, result, index };
            })
            .catch(error => {
                if (stopped) return { skipped: true };
                
                errors.push({ error, index });
                onError(error, index);
                
                if (failFast || errors.length >= maxErrors) {
                    stopped = true;
                }
                
                return { success: false, error, index };
            });
    });
    
    const allResults = await Promise.all(wrappedTasks);
    
    for (const item of allResults) {
        if (item.skipped) continue;
        if (item.success) {
            results.push(item.result);
        }
    }
    
    return {
        results,
        errors: errors.map(e => e.error),
        stats: {
            total: tasks.length,
            succeeded: results.length,
            failed: errors.length,
            stopped
        }
    };
}
*/

// =============================================================================
// EXERCISE 6: Safe API Client
// Build an API client with comprehensive error handling
// =============================================================================

/*
 * TODO: Create ApiClient class with:
 * - Automatic retry for network errors
 * - Circuit breaker integration
 * - Request timeout handling
 * - Error transformation (HTTP errors → custom errors)
 * - Request/response interceptors
 */

class ApiClient {
  // YOUR CODE HERE:
}

// SOLUTION:
/*
class ApiClient {
    constructor(baseUrl, options = {}) {
        this.baseUrl = baseUrl;
        this.timeout = options.timeout || 5000;
        this.maxRetries = options.maxRetries || 3;
        this.circuitBreaker = new CircuitBreaker(options.circuitBreaker);
        this.interceptors = {
            request: [],
            response: [],
            error: []
        };
    }
    
    addRequestInterceptor(fn) {
        this.interceptors.request.push(fn);
    }
    
    addResponseInterceptor(fn) {
        this.interceptors.response.push(fn);
    }
    
    addErrorInterceptor(fn) {
        this.interceptors.error.push(fn);
    }
    
    async applyInterceptors(type, data) {
        let result = data;
        for (const interceptor of this.interceptors[type]) {
            result = await interceptor(result);
        }
        return result;
    }
    
    async request(endpoint, options = {}) {
        const url = this.baseUrl + endpoint;
        let config = { ...options, url };
        
        // Apply request interceptors
        config = await this.applyInterceptors('request', config);
        
        // Wrap in circuit breaker and retry logic
        return this.circuitBreaker.execute(async () => {
            return retryWithBackoff(
                async () => this.executeRequest(config),
                {
                    maxRetries: this.maxRetries,
                    shouldRetry: (error) => this.isRetryable(error)
                }
            );
        });
    }
    
    async executeRequest(config) {
        const controller = new AbortController();
        const timeoutId = setTimeout(
            () => controller.abort(),
            this.timeout
        );
        
        try {
            // Simulated fetch
            const response = await this.simulateFetch(config.url, {
                ...config,
                signal: controller.signal
            });
            
            clearTimeout(timeoutId);
            
            // Apply response interceptors
            return this.applyInterceptors('response', response);
        } catch (error) {
            clearTimeout(timeoutId);
            
            // Transform and apply error interceptors
            const transformedError = this.transformError(error, config);
            await this.applyInterceptors('error', transformedError);
            throw transformedError;
        }
    }
    
    simulateFetch(url, options) {
        return new Promise((resolve, reject) => {
            if (options.signal?.aborted) {
                reject(new Error('Request aborted'));
                return;
            }
            
            setTimeout(() => {
                if (url.includes('fail')) {
                    reject(new Error('Network error'));
                } else {
                    resolve({ data: 'success', url });
                }
            }, 100);
        });
    }
    
    transformError(error, config) {
        if (error.name === 'AbortError') {
            return new TimeoutError('request', this.timeout);
        }
        
        if (error.message.includes('Network')) {
            return new NetworkError(error.message, 0, config.url);
        }
        
        return error;
    }
    
    isRetryable(error) {
        return error instanceof NetworkError ||
               error instanceof TimeoutError;
    }
    
    get(endpoint, options = {}) {
        return this.request(endpoint, { ...options, method: 'GET' });
    }
    
    post(endpoint, data, options = {}) {
        return this.request(endpoint, { ...options, method: 'POST', body: data });
    }
}
*/

// =============================================================================
// EXERCISE 7: Error Logger with Context
// Create a centralized error logging system
// =============================================================================

/*
 * TODO: Create ErrorLogger class that:
 * - Captures error with full context (stack, timestamp, user, request)
 * - Supports different log levels (error, warning, info)
 * - Batches errors and sends to server periodically
 * - Deduplicates similar errors
 * - Provides error statistics
 */

class ErrorLogger {
  // YOUR CODE HERE:
}

// SOLUTION:
/*
class ErrorLogger {
    constructor(options = {}) {
        this.buffer = [];
        this.batchSize = options.batchSize || 10;
        this.flushInterval = options.flushInterval || 5000;
        this.endpoint = options.endpoint || '/api/logs';
        this.context = options.context || {};
        this.errorCounts = new Map();
        
        setInterval(() => this.flush(), this.flushInterval);
    }
    
    setContext(key, value) {
        this.context[key] = value;
    }
    
    log(error, level = 'error', additionalContext = {}) {
        const errorKey = this.getErrorKey(error);
        const count = (this.errorCounts.get(errorKey) || 0) + 1;
        this.errorCounts.set(errorKey, count);
        
        const entry = {
            level,
            message: error.message,
            name: error.name,
            stack: error.stack,
            timestamp: new Date().toISOString(),
            context: { ...this.context, ...additionalContext },
            count,
            fingerprint: errorKey
        };
        
        this.buffer.push(entry);
        
        if (this.buffer.length >= this.batchSize) {
            this.flush();
        }
        
        return entry;
    }
    
    error(error, context = {}) {
        return this.log(error, 'error', context);
    }
    
    warning(error, context = {}) {
        return this.log(error, 'warning', context);
    }
    
    info(message, context = {}) {
        return this.log(new Error(message), 'info', context);
    }
    
    getErrorKey(error) {
        // Create fingerprint from error type and first line of stack
        const stackLine = error.stack?.split('\n')[1] || '';
        return `${error.name}:${error.message}:${stackLine.trim()}`;
    }
    
    deduplicate(entries) {
        const seen = new Map();
        
        return entries.filter(entry => {
            if (seen.has(entry.fingerprint)) {
                const existing = seen.get(entry.fingerprint);
                existing.count = entry.count;
                return false;
            }
            seen.set(entry.fingerprint, entry);
            return true;
        });
    }
    
    async flush() {
        if (this.buffer.length === 0) return;
        
        const entries = this.deduplicate([...this.buffer]);
        this.buffer = [];
        
        console.log(`Flushing ${entries.length} log entries...`);
        
        // Simulate sending to server
        try {
            // await fetch(this.endpoint, {
            //     method: 'POST',
            //     body: JSON.stringify(entries)
            // });
            console.log('Logs sent:', entries);
        } catch (error) {
            // Re-add to buffer if send fails
            this.buffer.unshift(...entries);
            console.error('Failed to send logs:', error);
        }
    }
    
    getStats() {
        return {
            buffered: this.buffer.length,
            uniqueErrors: this.errorCounts.size,
            topErrors: [...this.errorCounts.entries()]
                .sort((a, b) => b[1] - a[1])
                .slice(0, 5)
        };
    }
}
*/

// =============================================================================
// EXERCISE 8: Graceful Degradation
// Implement a system that gracefully degrades when errors occur
// =============================================================================

/*
 * TODO: Create FeatureManager class that:
 * - Tracks feature health based on errors
 * - Automatically disables features that fail too often
 * - Provides fallback behavior when features are disabled
 * - Allows manual feature toggling
 * - Periodically retries disabled features
 */

class FeatureManager {
  // YOUR CODE HERE:
}

// SOLUTION:
/*
class FeatureManager {
    constructor(options = {}) {
        this.features = new Map();
        this.errorThreshold = options.errorThreshold || 3;
        this.recoveryInterval = options.recoveryInterval || 60000;
        
        setInterval(() => this.attemptRecovery(), this.recoveryInterval);
    }
    
    register(name, implementation, fallback) {
        this.features.set(name, {
            name,
            implementation,
            fallback,
            enabled: true,
            errorCount: 0,
            lastError: null,
            disabledAt: null
        });
    }
    
    async execute(name, ...args) {
        const feature = this.features.get(name);
        
        if (!feature) {
            throw new Error(`Feature "${name}" not registered`);
        }
        
        if (!feature.enabled) {
            console.log(`Feature "${name}" disabled, using fallback`);
            return feature.fallback(...args);
        }
        
        try {
            const result = await feature.implementation(...args);
            feature.errorCount = 0; // Reset on success
            return result;
        } catch (error) {
            feature.errorCount++;
            feature.lastError = { error, timestamp: Date.now() };
            
            if (feature.errorCount >= this.errorThreshold) {
                this.disable(name);
            }
            
            console.log(`Feature "${name}" error (${feature.errorCount}), using fallback`);
            return feature.fallback(...args);
        }
    }
    
    disable(name) {
        const feature = this.features.get(name);
        if (feature) {
            feature.enabled = false;
            feature.disabledAt = Date.now();
            console.log(`Feature "${name}" has been disabled`);
        }
    }
    
    enable(name) {
        const feature = this.features.get(name);
        if (feature) {
            feature.enabled = true;
            feature.errorCount = 0;
            feature.disabledAt = null;
            console.log(`Feature "${name}" has been enabled`);
        }
    }
    
    attemptRecovery() {
        for (const [name, feature] of this.features) {
            if (!feature.enabled && feature.disabledAt) {
                console.log(`Attempting to recover feature "${name}"`);
                feature.enabled = true;
                feature.errorCount = 0;
                // First error will re-disable if still failing
            }
        }
    }
    
    getStatus() {
        const status = {};
        for (const [name, feature] of this.features) {
            status[name] = {
                enabled: feature.enabled,
                errorCount: feature.errorCount,
                lastError: feature.lastError?.error?.message
            };
        }
        return status;
    }
}
*/

// =============================================================================
// TEST YOUR IMPLEMENTATIONS
// =============================================================================

async function runTests() {
  console.log('Testing Custom Error Classes...');
  // Test Exercise 1

  console.log('\nTesting Error Handler...');
  // Test Exercise 2

  console.log('\nTesting Retry with Backoff...');
  // Test Exercise 3

  console.log('\nTesting Circuit Breaker...');
  // Test Exercise 4

  console.log('\nTesting Parallel Runner...');
  // Test Exercise 5

  console.log('\nAll tests complete!');
}

// Uncomment to run tests
// runTests();
Exercises - JavaScript Tutorial | DeepML