javascript

exercises

exercises.js
/**
 * 4.7 Error Types and Custom Errors - Exercises
 */

/**
 * Exercise 1: Error Type Identification
 * Create a function that catches an error and identifies its type
 */
function identifyError(fn) {
  try {
    fn();
    return 'No error';
  } catch (error) {
    // TODO: Return the specific error type as a string
    // Handle: TypeError, ReferenceError, RangeError, SyntaxError, URIError, Error
    // Your code here
  }
}

// Test cases:
// console.log(identifyError(() => null.property));     // "TypeError"
// console.log(identifyError(() => nonExistent));       // "ReferenceError"
// console.log(identifyError(() => new Array(-1)));     // "RangeError"
// console.log(identifyError(() => JSON.parse('{')));   // "SyntaxError"

/**
 * Exercise 2: Basic Custom Error
 * Create a custom error for invalid age
 */
class InvalidAgeError extends Error {
  // TODO: Implement this class
  // - Constructor takes age and message
  // - Store the age value
  // - Set appropriate name
}

function validateAge(age) {
  // TODO: Throw InvalidAgeError if age < 0 or age > 150
  // Return true if valid
}

// Test cases:
// try {
//     validateAge(-5);
// } catch (e) {
//     console.log(e.name);     // "InvalidAgeError"
//     console.log(e.age);      // -5
//     console.log(e.message);  // "Age cannot be negative"
// }

/**
 * Exercise 3: Error Hierarchy
 * Create a payment error hierarchy
 */

// TODO: Create base PaymentError class with:
// - message, code, timestamp properties
// - toJSON() method

// TODO: Create InsufficientFundsError extending PaymentError
// - Takes amount and balance
// - Code should be 'INSUFFICIENT_FUNDS'

// TODO: Create InvalidCardError extending PaymentError
// - Takes cardNumber (last 4 digits only)
// - Code should be 'INVALID_CARD'

// TODO: Create PaymentDeclinedError extending PaymentError
// - Takes reason
// - Code should be 'PAYMENT_DECLINED'

/**
 * Exercise 4: Error with Cause Chain
 * Implement functions that chain error causes
 */
function parseJSON(jsonString) {
  // TODO: Parse JSON and wrap any errors with cause
  // Throw new Error with message "JSON parsing failed" and cause
}

function loadData(jsonString) {
  // TODO: Call parseJSON and wrap any errors
  // Throw new Error with message "Failed to load data" and cause
}

function processData(jsonString) {
  // TODO: Call loadData and wrap any errors
  // Throw new Error with message "Data processing failed" and cause
}

// Implement a function to get the full error chain
function getErrorChain(error) {
  // TODO: Return array of all error messages in the chain
  // Example: ["Data processing failed", "Failed to load data", "JSON parsing failed", "..."]
}

// Test:
// try {
//     processData('{invalid}');
// } catch (e) {
//     console.log(getErrorChain(e));
// }

/**
 * Exercise 5: Form Validation Error
 * Create a comprehensive form validation error class
 */
class FieldValidationError {
  // TODO: Implement
  // - field: string
  // - value: any
  // - rules: string[] (failed validation rules)
}

class FormValidationError extends Error {
  // TODO: Implement
  // - fieldErrors: FieldValidationError[]
  // - getErrorsForField(field): string[]
  // - hasError(field): boolean
  // - getAllErrors(): object with all fields and their errors
}

// Create a validator function
function validateRegistrationForm(data) {
  // TODO: Validate:
  // - username: required, min 3 chars, max 20 chars
  // - email: required, must contain @
  // - password: required, min 8 chars, must contain number
  // - confirmPassword: must match password
  // Throw FormValidationError if any validation fails
}

// Test:
// try {
//     validateRegistrationForm({
//         username: 'ab',
//         email: 'invalid',
//         password: 'short',
//         confirmPassword: 'different'
//     });
// } catch (e) {
//     console.log(e.getAllErrors());
// }

/**
 * Exercise 6: HTTP Error Handler
 * Create a robust HTTP error handling system
 */

// TODO: Create these HTTP error classes:
// - BadRequestError (400)
// - UnauthorizedError (401)
// - ForbiddenError (403)
// - NotFoundError (404)
// - InternalServerError (500)

// All should extend a base HttpError class with:
// - statusCode, statusText, body, headers properties
// - toResponse() method returning { status, statusText, body }

// Create a factory function
function createHttpError(statusCode, message, details = {}) {
  // TODO: Return appropriate error class based on statusCode
}

// Create an error handler
function handleHttpError(error) {
  // TODO: Return standardized error response object
  // { success: false, error: { code, message, details } }
}

/**
 * Exercise 7: Async Operation Error
 * Handle errors in async operations with retries
 */
class RetryableError extends Error {
  // TODO: Implement
  // - retryable: boolean
  // - retryAfter: number (seconds)
  // - attempts: number
}

async function withRetry(fn, maxAttempts = 3, delay = 1000) {
  // TODO: Implement retry logic
  // - Call fn()
  // - If it throws RetryableError and retryable is true, retry
  // - Wait 'delay' ms between retries (use delay * attempt for exponential backoff)
  // - After maxAttempts, throw the last error with all attempts info
}

// Test:
// let attempts = 0;
// await withRetry(async () => {
//     attempts++;
//     if (attempts < 3) {
//         const err = new RetryableError('Temporary failure');
//         err.retryable = true;
//         throw err;
//     }
//     return 'Success!';
// });

/**
 * Exercise 8: Error Aggregation
 * Collect multiple errors and report them together
 */
class BulkOperationError extends Error {
  // TODO: Implement
  // - successCount: number
  // - failureCount: number
  // - errors: array of { index, error, item }
  // - getFailedItems(): array of items that failed
  // - getSummary(): string summary of operation
}

async function processBulk(items, processor) {
  // TODO: Process all items, collecting errors
  // - Don't stop on individual errors
  // - At the end, if any errors occurred, throw BulkOperationError
  // - Return array of successful results
}

// Test:
// const items = [1, 2, 'invalid', 4, null, 6];
// try {
//     await processBulk(items, async (item) => {
//         if (typeof item !== 'number') {
//             throw new Error(`Invalid item: ${item}`);
//         }
//         return item * 2;
//     });
// } catch (e) {
//     console.log(e.getSummary());
//     console.log(e.getFailedItems());
// }

/**
 * Exercise 9: Error Serialization
 * Create errors that serialize properly for logging/API responses
 */
class SerializableError extends Error {
  // TODO: Implement
  // - toJSON(): return full error info as plain object
  // - toString(): return formatted string
  // - static fromJSON(json): recreate error from JSON
}

// Create specific serializable errors
class DatabaseError extends SerializableError {
  // TODO: Include query, params, connection info
}

class ExternalServiceError extends SerializableError {
  // TODO: Include service name, endpoint, response time
}

/**
 * Exercise 10: Error Factory with Context
 * Create a factory that adds context to errors
 */
function createErrorFactory(context) {
  // TODO: Return an object with methods to create contextualized errors
  // - validation(field, message): create validation error with context
  // - notFound(resource, id): create not found error with context
  // - permission(action, resource): create permission error with context
  // - internal(message, cause): create internal error with context
  // Each error should include:
  // - The passed context (userId, requestId, etc.)
  // - Timestamp
  // - Stack trace
}

// Usage:
// const errors = createErrorFactory({
//     userId: 'user123',
//     requestId: 'req-456',
//     service: 'user-service'
// });
//
// throw errors.notFound('User', 42);
// throw errors.validation('email', 'Invalid format');

/**
 * BONUS CHALLENGES
 */

/**
 * Bonus 1: Error Boundary Pattern
 * Implement a class that acts as an error boundary
 */
class ErrorBoundary {
  // TODO: Implement
  // - onError callback for handling errors
  // - wrap(fn) method that catches errors and calls onError
  // - wrapAsync(fn) for async functions
  // - recover(fn) method to provide fallback value on error
}

/**
 * Bonus 2: Type-Safe Error Codes
 * Create a system with predefined error codes
 */
const ErrorCodes = {
  // TODO: Define error codes with metadata
  // E001: { message: '...', severity: '...', retryable: boolean }
};

class CodedError extends Error {
  // TODO: Create error from ErrorCodes
  // constructor(code, details?)
}

// ============================================
// SOLUTION KEY (for reference)
// ============================================

/*
// Exercise 1 Solution:
function identifyError(fn) {
    try {
        fn();
        return 'No error';
    } catch (error) {
        if (error instanceof TypeError) return 'TypeError';
        if (error instanceof ReferenceError) return 'ReferenceError';
        if (error instanceof RangeError) return 'RangeError';
        if (error instanceof SyntaxError) return 'SyntaxError';
        if (error instanceof URIError) return 'URIError';
        return 'Error';
    }
}

// Exercise 2 Solution:
class InvalidAgeError extends Error {
    constructor(age, message) {
        super(message || `Invalid age: ${age}`);
        this.name = 'InvalidAgeError';
        this.age = age;
    }
}

function validateAge(age) {
    if (age < 0) {
        throw new InvalidAgeError(age, 'Age cannot be negative');
    }
    if (age > 150) {
        throw new InvalidAgeError(age, 'Age cannot exceed 150');
    }
    return true;
}

// Exercise 3 Solution:
class PaymentError extends Error {
    constructor(message, code) {
        super(message);
        this.name = 'PaymentError';
        this.code = code;
        this.timestamp = new Date();
    }
    
    toJSON() {
        return {
            name: this.name,
            message: this.message,
            code: this.code,
            timestamp: this.timestamp.toISOString()
        };
    }
}

class InsufficientFundsError extends PaymentError {
    constructor(amount, balance) {
        super(`Insufficient funds: need ${amount}, have ${balance}`, 'INSUFFICIENT_FUNDS');
        this.amount = amount;
        this.balance = balance;
    }
}

class InvalidCardError extends PaymentError {
    constructor(cardNumber) {
        const last4 = String(cardNumber).slice(-4);
        super(`Invalid card ending in ${last4}`, 'INVALID_CARD');
        this.last4 = last4;
    }
}

class PaymentDeclinedError extends PaymentError {
    constructor(reason) {
        super(`Payment declined: ${reason}`, 'PAYMENT_DECLINED');
        this.reason = reason;
    }
}

// Exercise 4 Solution:
function parseJSON(jsonString) {
    try {
        return JSON.parse(jsonString);
    } catch (error) {
        throw new Error('JSON parsing failed', { cause: error });
    }
}

function loadData(jsonString) {
    try {
        return parseJSON(jsonString);
    } catch (error) {
        throw new Error('Failed to load data', { cause: error });
    }
}

function processData(jsonString) {
    try {
        return loadData(jsonString);
    } catch (error) {
        throw new Error('Data processing failed', { cause: error });
    }
}

function getErrorChain(error) {
    const chain = [];
    let current = error;
    while (current) {
        chain.push(current.message);
        current = current.cause;
    }
    return chain;
}
*/

console.log('Complete the exercises above!');
console.log('Check the solution key at the bottom for guidance.');
Exercises - JavaScript Tutorial | DeepML