javascript

exercises

exercises.js
/**
 * =====================================================
 * 4.6 ERROR HANDLING WITH TRY-CATCH - EXERCISES
 * =====================================================
 * Practice error handling
 */

/**
 * Exercise 1: Safe JSON Parse
 *
 * Create a function that safely parses JSON.
 * Return the parsed object or a default value if parsing fails.
 */
function safeJSONParse(jsonString, defaultValue = null) {
  // TODO: Use try-catch to safely parse JSON
}

// Test cases:
console.log('Exercise 1:');
console.log(safeJSONParse('{"name": "John"}')); // { name: "John" }
console.log(safeJSONParse('invalid')); // null
console.log(safeJSONParse('bad', [])); // []

/**
 * Exercise 2: Division with Validation
 *
 * Create a divide function that:
 * - Throws TypeError if arguments aren't numbers
 * - Throws RangeError if dividing by zero
 * - Returns the result otherwise
 */
function safeDivide(a, b) {
  // TODO: Implement with proper error throwing
}

// Test cases:
console.log('\nExercise 2:');
try {
  console.log(safeDivide(10, 2)); // 5
  console.log(safeDivide(10, 0)); // Should throw
} catch (e) {
  console.log(`${e.name}: ${e.message}`);
}

/**
 * Exercise 3: Validate Email
 *
 * Create a function that validates an email:
 * - Throws if email is not a string
 * - Throws if email doesn't contain @
 * - Throws if email is empty
 * - Returns true if valid
 */
function validateEmail(email) {
  // TODO: Implement email validation with throws
}

// Test cases:
console.log('\nExercise 3:');
try {
  console.log(validateEmail('user@example.com')); // true
  console.log(validateEmail('invalid')); // throws
} catch (e) {
  console.log(`Error: ${e.message}`);
}

/**
 * Exercise 4: Array Access with Error Handling
 *
 * Create a function that safely gets an array element.
 * Returns the element or undefined if index is out of bounds.
 * Throws if arr is not an array.
 */
function safeArrayAccess(arr, index) {
  // TODO: Implement safe array access
}

// Test cases:
console.log('\nExercise 4:');
console.log(safeArrayAccess([1, 2, 3], 1)); // 2
console.log(safeArrayAccess([1, 2, 3], 10)); // undefined
try {
  console.log(safeArrayAccess('not array', 0)); // throws
} catch (e) {
  console.log(`Error: ${e.message}`);
}

/**
 * Exercise 5: Custom Validation Error
 *
 * Create a ValidationError class that includes:
 * - message: error message
 * - field: the field that failed validation
 *
 * Then create a validateUser function that uses it.
 */
class ValidationError extends Error {
  // TODO: Implement custom error class
}

function validateUser(user) {
  // TODO: Validate user object and throw ValidationError
  // Required: name (string, min 2 chars), age (number, min 0)
}

// Test cases:
console.log('\nExercise 5:');
try {
  validateUser({ name: 'A', age: 25 });
} catch (e) {
  if (e instanceof ValidationError) {
    console.log(`Validation error on '${e.field}': ${e.message}`);
  }
}

/**
 * Exercise 6: Try-Finally Cleanup
 *
 * Create a function that simulates opening, using, and closing a resource.
 * The resource should always be closed even if an error occurs.
 */
function useResource(shouldFail) {
  let resource = null;

  // TODO: Implement with try-finally
  // - Set resource to { open: true }
  // - If shouldFail, throw an error
  // - In finally, log "Resource closed" and set resource.open to false
  // - Return "Success" if no error
}

// Test cases:
console.log('\nExercise 6:');
try {
  console.log(useResource(false)); // Should log "Resource closed", return "Success"
  console.log(useResource(true)); // Should log "Resource closed", throw error
} catch (e) {
  console.log(`Error: ${e.message}`);
}

/**
 * Exercise 7: Error Type Handler
 *
 * Create a function that takes an error and returns a user-friendly message
 * based on the error type.
 */
function getErrorMessage(error) {
  // TODO: Return different messages based on error type
  // TypeError: "There was a type mismatch"
  // RangeError: "Value was out of acceptable range"
  // SyntaxError: "Invalid syntax detected"
  // Other: "An unexpected error occurred"
}

// Test cases:
console.log('\nExercise 7:');
console.log(getErrorMessage(new TypeError('test')));
console.log(getErrorMessage(new RangeError('test')));
console.log(getErrorMessage(new SyntaxError('test')));
console.log(getErrorMessage(new Error('test')));

/**
 * Exercise 8: Safe Function Wrapper
 *
 * Create a higher-order function that wraps any function
 * and catches any errors, returning undefined instead.
 */
function makeSafe(fn) {
  // TODO: Return a new function that catches errors
}

// Test cases:
console.log('\nExercise 8:');
const riskyFn = (x) => {
  if (x < 0) throw new Error('Negative!');
  return x * 2;
};

const safeFn = makeSafe(riskyFn);
console.log(safeFn(5)); // 10
console.log(safeFn(-5)); // undefined

/**
 * Exercise 9: Multiple Validation
 *
 * Create a function that validates a form object and collects
 * ALL errors instead of stopping at the first one.
 * Return { valid: true } or { valid: false, errors: [...] }
 */
function validateForm(form) {
  // TODO: Validate form with all errors collected
  // Required fields: name (string), email (contains @), age (number >= 0)
}

// Test cases:
console.log('\nExercise 9:');
console.log(validateForm({ name: 'John', email: 'john@example.com', age: 25 }));
// { valid: true }
console.log(validateForm({ name: '', email: 'invalid', age: -5 }));
// { valid: false, errors: [...3 errors...] }

/**
 * Exercise 10: Retry with Error Handling
 *
 * Create a function that attempts an operation up to maxRetries times.
 * Returns the result if successful, throws if all retries fail.
 */
function retry(fn, maxRetries = 3) {
  // TODO: Implement retry logic with try-catch
}

// Test cases:
console.log('\nExercise 10:');
let attempts = 0;
const unreliableFn = () => {
  attempts++;
  if (attempts < 3) throw new Error('Failed');
  return 'Success!';
};

try {
  attempts = 0;
  console.log(retry(unreliableFn, 5)); // "Success!" (on 3rd attempt)
} catch (e) {
  console.log(`Error: ${e.message}`);
}

// =====================================================
// BONUS CHALLENGES
// =====================================================

/**
 * Bonus 1: Error Chain
 *
 * Create a function that preserves the original error
 * while adding context.
 */
class ChainedError extends Error {
  // TODO: Implement error that includes cause
}

function processData(data) {
  // TODO: Parse data and add context to any errors
}

/**
 * Bonus 2: Async-Like Error Handling
 *
 * Create a Result type that wraps success/failure
 * without using try-catch at the call site.
 */
class Result {
  // TODO: Implement Ok and Err result types
}

/**
 * Bonus 3: Error Boundary
 *
 * Create a function that runs multiple operations
 * and returns partial results for successful ones
 * along with errors for failed ones.
 */
function runOperations(operations) {
  // TODO: Run all operations, collect results and errors
}

console.log('\nBonus 3:');
const ops = [
  () => 1 + 1,
  () => {
    throw new Error('Failed');
  },
  () => 2 + 2,
];
// console.log(runOperations(ops));
// { results: [2, 4], errors: [Error: "Failed"], indices: { success: [0, 2], failed: [1] } }

// =====================================================
// SOLUTIONS (Uncomment to check your answers)
// =====================================================

/*
// Exercise 1 Solution:
function safeJSONParse(jsonString, defaultValue = null) {
    try {
        return JSON.parse(jsonString);
    } catch {
        return defaultValue;
    }
}

// Exercise 2 Solution:
function safeDivide(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;
}

// Exercise 3 Solution:
function validateEmail(email) {
    if (typeof email !== "string") {
        throw new TypeError("Email must be a string");
    }
    if (email.length === 0) {
        throw new Error("Email cannot be empty");
    }
    if (!email.includes("@")) {
        throw new Error("Email must contain @");
    }
    return true;
}

// Exercise 4 Solution:
function safeArrayAccess(arr, index) {
    if (!Array.isArray(arr)) {
        throw new TypeError("First argument must be an array");
    }
    if (index < 0 || index >= arr.length) {
        return undefined;
    }
    return arr[index];
}

// Exercise 5 Solution:
class ValidationError extends Error {
    constructor(message, field) {
        super(message);
        this.name = "ValidationError";
        this.field = field;
    }
}

function validateUser(user) {
    if (typeof user.name !== "string" || user.name.length < 2) {
        throw new ValidationError("Name must be at least 2 characters", "name");
    }
    if (typeof user.age !== "number" || user.age < 0) {
        throw new ValidationError("Age must be a non-negative number", "age");
    }
    return true;
}

// Exercise 6 Solution:
function useResource(shouldFail) {
    let resource = null;
    try {
        resource = { open: true };
        if (shouldFail) {
            throw new Error("Operation failed");
        }
        return "Success";
    } finally {
        if (resource) {
            console.log("Resource closed");
            resource.open = false;
        }
    }
}

// Exercise 7 Solution:
function getErrorMessage(error) {
    if (error instanceof TypeError) {
        return "There was a type mismatch";
    } else if (error instanceof RangeError) {
        return "Value was out of acceptable range";
    } else if (error instanceof SyntaxError) {
        return "Invalid syntax detected";
    } else {
        return "An unexpected error occurred";
    }
}

// Exercise 8 Solution:
function makeSafe(fn) {
    return function(...args) {
        try {
            return fn(...args);
        } catch {
            return undefined;
        }
    };
}

// Exercise 9 Solution:
function validateForm(form) {
    const errors = [];
    
    if (typeof form.name !== "string" || form.name.length === 0) {
        errors.push("Name is required");
    }
    if (typeof form.email !== "string" || !form.email.includes("@")) {
        errors.push("Valid email is required");
    }
    if (typeof form.age !== "number" || form.age < 0) {
        errors.push("Age must be a non-negative number");
    }
    
    return errors.length === 0
        ? { valid: true }
        : { valid: false, errors };
}

// Exercise 10 Solution:
function retry(fn, maxRetries = 3) {
    let lastError;
    for (let i = 0; i < maxRetries; i++) {
        try {
            return fn();
        } catch (error) {
            lastError = error;
        }
    }
    throw lastError;
}

// Bonus 1 Solution:
class ChainedError extends Error {
    constructor(message, cause) {
        super(message);
        this.name = "ChainedError";
        this.cause = cause;
    }
}

function processData(data) {
    try {
        return JSON.parse(data);
    } catch (error) {
        throw new ChainedError("Failed to process data", error);
    }
}

// Bonus 3 Solution:
function runOperations(operations) {
    const results = [];
    const errors = [];
    const indices = { success: [], failed: [] };
    
    for (let i = 0; i < operations.length; i++) {
        try {
            results.push(operations[i]());
            indices.success.push(i);
        } catch (error) {
            errors.push(error);
            indices.failed.push(i);
        }
    }
    
    return { results, errors, indices };
}
*/
Exercises - JavaScript Tutorial | DeepML