javascript

exercises

exercises.js
/**
 * ============================================================
 * 9.4 ERROR HANDLING IN ASYNC CODE - EXERCISES
 * ============================================================
 *
 * Complete each exercise by implementing the error handling patterns.
 * Test your solutions by running this file.
 */

// Helper functions
function delay(ms, value) {
  return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}

function delayReject(ms, message) {
  return new Promise((_, reject) =>
    setTimeout(() => reject(new Error(message)), ms)
  );
}

/**
 * EXERCISE 1: Basic try/catch
 *
 * Create an async function 'safeDivide' that:
 * - Takes two numbers (a, b)
 * - Returns a / b
 * - If b is 0, catches the error and returns null
 * - Logs "Division by zero" when catching
 */

// TODO: Implement safeDivide
async function safeDivide(a, b) {
  // Your code here
}

// Test
// safeDivide(10, 2).then(r => console.log('Exercise 1:', r));  // 5
// safeDivide(10, 0).then(r => console.log('Exercise 1:', r));  // null

/*
 * SOLUTION 1:
 *
 * async function safeDivide(a, b) {
 *     try {
 *         if (b === 0) throw new Error("Division by zero");
 *         return a / b;
 *     } catch (error) {
 *         console.log("Division by zero");
 *         return null;
 *     }
 * }
 */

/**
 * EXERCISE 2: Error Tuple Pattern
 *
 * Create a function 'wrapAsync' that:
 * - Takes a promise
 * - Returns [error, result] tuple
 * - If success: [null, result]
 * - If error: [error, null]
 */

// TODO: Implement wrapAsync
async function wrapAsync(promise) {
  // Your code here
}

// Test
// wrapAsync(delay(50, 'data')).then(r => console.log('Exercise 2:', r));
// wrapAsync(delayReject(50, 'error')).then(r => console.log('Exercise 2:', r));

/*
 * SOLUTION 2:
 *
 * async function wrapAsync(promise) {
 *     try {
 *         const result = await promise;
 *         return [null, result];
 *     } catch (error) {
 *         return [error, null];
 *     }
 * }
 */

/**
 * EXERCISE 3: Result Object Pattern
 *
 * Create a class 'Result' with:
 * - Static method 'ok(value)' - returns success result
 * - Static method 'fail(error)' - returns failure result
 * - Instance method 'isOk()' - returns true if success
 * - Instance method 'unwrap()' - returns value or throws if error
 * - Instance method 'unwrapOr(defaultValue)' - returns value or default
 */

// TODO: Implement Result class
class Result {
  // Your code here
}

// Test
// const success = Result.ok(42);
// const failure = Result.fail(new Error('failed'));
// console.log('Exercise 3:', success.isOk(), success.unwrap());
// console.log('Exercise 3:', failure.isOk(), failure.unwrapOr(0));

/*
 * SOLUTION 3:
 *
 * class Result {
 *     constructor(value, error) {
 *         this.value = value;
 *         this.error = error;
 *     }
 *
 *     static ok(value) {
 *         return new Result(value, null);
 *     }
 *
 *     static fail(error) {
 *         return new Result(null, error);
 *     }
 *
 *     isOk() {
 *         return this.error === null;
 *     }
 *
 *     unwrap() {
 *         if (this.error) throw this.error;
 *         return this.value;
 *     }
 *
 *     unwrapOr(defaultValue) {
 *         return this.error ? defaultValue : this.value;
 *     }
 * }
 */

/**
 * EXERCISE 4: Custom Error Type
 *
 * Create a custom error class 'APIError' that:
 * - Extends Error
 * - Has properties: statusCode, endpoint, retryable
 * - statusCode >= 500 should be retryable
 * - Has method 'toJSON()' that returns error details
 */

// TODO: Implement APIError
class APIError extends Error {
  // Your code here
}

// Test
// const err = new APIError('Not found', 404, '/api/users');
// console.log('Exercise 4:', err.retryable, err.toJSON());

/*
 * SOLUTION 4:
 *
 * class APIError extends Error {
 *     constructor(message, statusCode, endpoint) {
 *         super(message);
 *         this.name = 'APIError';
 *         this.statusCode = statusCode;
 *         this.endpoint = endpoint;
 *         this.retryable = statusCode >= 500;
 *     }
 *
 *     toJSON() {
 *         return {
 *             name: this.name,
 *             message: this.message,
 *             statusCode: this.statusCode,
 *             endpoint: this.endpoint,
 *             retryable: this.retryable
 *         };
 *     }
 * }
 */

/**
 * EXERCISE 5: Retry with Custom Logic
 *
 * Create an async function 'retryIf' that:
 * - Takes (asyncFn, shouldRetry, maxRetries)
 * - Calls asyncFn()
 * - If it throws, calls shouldRetry(error) to decide if retry
 * - Retries up to maxRetries times if shouldRetry returns true
 * - Throws immediately if shouldRetry returns false
 */

// TODO: Implement retryIf
async function retryIf(asyncFn, shouldRetry, maxRetries) {
  // Your code here
}

// Test
// let count = 0;
// const fn = async () => { count++; throw new Error(count < 3 ? 'retry' : 'stop'); };
// const canRetry = err => err.message === 'retry';
// retryIf(fn, canRetry, 5).catch(e => console.log('Exercise 5:', e.message, 'after', count));

/*
 * SOLUTION 5:
 *
 * async function retryIf(asyncFn, shouldRetry, maxRetries) {
 *     for (let i = 0; i < maxRetries; i++) {
 *         try {
 *             return await asyncFn();
 *         } catch (error) {
 *             if (!shouldRetry(error) || i === maxRetries - 1) {
 *                 throw error;
 *             }
 *         }
 *     }
 * }
 */

/**
 * EXERCISE 6: Error Recovery Chain
 *
 * Create an async function 'tryInOrder' that:
 * - Takes an array of async functions
 * - Tries each one in order
 * - Returns first successful result
 * - If all fail, throws an AggregateError with all errors
 */

// TODO: Implement tryInOrder
async function tryInOrder(asyncFns) {
  // Your code here
}

// Test
// tryInOrder([
//     () => delayReject(50, 'first failed'),
//     () => delayReject(50, 'second failed'),
//     () => delay(50, 'third succeeded')
// ]).then(r => console.log('Exercise 6:', r));

/*
 * SOLUTION 6:
 *
 * async function tryInOrder(asyncFns) {
 *     const errors = [];
 *     for (const fn of asyncFns) {
 *         try {
 *             return await fn();
 *         } catch (error) {
 *             errors.push(error);
 *         }
 *     }
 *     throw new AggregateError(errors, 'All attempts failed');
 * }
 */

/**
 * EXERCISE 7: Partial Success Handler
 *
 * Create an async function 'collectResults' that:
 * - Takes an array of promises
 * - Returns { successes: [...values], failures: [...errors] }
 * - Waits for all to complete (don't fail fast)
 */

// TODO: Implement collectResults
async function collectResults(promises) {
  // Your code here
}

// Test
// collectResults([
//     delay(50, 'a'),
//     delayReject(50, 'error1'),
//     delay(50, 'b'),
//     delayReject(50, 'error2')
// ]).then(r => console.log('Exercise 7:', r));

/*
 * SOLUTION 7:
 *
 * async function collectResults(promises) {
 *     const results = await Promise.allSettled(promises);
 *     return {
 *         successes: results
 *             .filter(r => r.status === 'fulfilled')
 *             .map(r => r.value),
 *         failures: results
 *             .filter(r => r.status === 'rejected')
 *             .map(r => r.reason)
 *     };
 * }
 */

/**
 * EXERCISE 8: Resource Cleanup Pattern
 *
 * Create an async function 'withResource' that:
 * - Takes (acquire, use, release)
 * - acquire() returns a resource
 * - use(resource) does work with the resource
 * - release(resource) cleans up the resource
 * - Always calls release, even if use() throws
 * - Returns the result of use()
 */

// TODO: Implement withResource
async function withResource(acquire, use, release) {
  // Your code here
}

// Test
// withResource(
//     async () => { console.log('Acquired'); return { id: 1 }; },
//     async (r) => { console.log('Using', r.id); return 'result'; },
//     async (r) => { console.log('Released', r.id); }
// ).then(r => console.log('Exercise 8:', r));

/*
 * SOLUTION 8:
 *
 * async function withResource(acquire, use, release) {
 *     const resource = await acquire();
 *     try {
 *         return await use(resource);
 *     } finally {
 *         await release(resource);
 *     }
 * }
 */

/**
 * EXERCISE 9: Timeout with Fallback
 *
 * Create an async function 'withTimeoutFallback' that:
 * - Takes (promise, timeoutMs, fallbackValue)
 * - Returns promise result if it resolves in time
 * - Returns fallbackValue if timeout occurs
 * - Does NOT throw on timeout
 */

// TODO: Implement withTimeoutFallback
async function withTimeoutFallback(promise, timeoutMs, fallbackValue) {
  // Your code here
}

// Test
// withTimeoutFallback(delay(50, 'fast'), 100, 'default').then(r => console.log('Exercise 9:', r));
// withTimeoutFallback(delay(200, 'slow'), 100, 'default').then(r => console.log('Exercise 9:', r));

/*
 * SOLUTION 9:
 *
 * async function withTimeoutFallback(promise, timeoutMs, fallbackValue) {
 *     const timeout = delay(timeoutMs).then(() => fallbackValue);
 *     return Promise.race([promise, timeout]);
 * }
 */

/**
 * EXERCISE 10: Circuit Breaker
 *
 * Create a class 'CircuitBreaker' that:
 * - Constructor takes { threshold, resetTimeout }
 * - Method 'execute(asyncFn)' runs the function
 * - Opens circuit after 'threshold' consecutive failures
 * - Throws immediately when open
 * - Resets to closed after 'resetTimeout' ms
 * - Has method 'getState()' returning 'CLOSED' | 'OPEN'
 */

// TODO: Implement CircuitBreaker
class CircuitBreaker {
  // Your code here
}

// Test
// const cb = new CircuitBreaker({ threshold: 2, resetTimeout: 500 });
// const fail = async () => { throw new Error('fail'); };
// cb.execute(fail).catch(() => console.log('Exercise 10: State:', cb.getState()));
// cb.execute(fail).catch(() => console.log('Exercise 10: State:', cb.getState()));
// cb.execute(fail).catch(e => console.log('Exercise 10: Circuit open?', e.message));

/*
 * SOLUTION 10:
 *
 * class CircuitBreaker {
 *     constructor({ threshold, resetTimeout }) {
 *         this.threshold = threshold;
 *         this.resetTimeout = resetTimeout;
 *         this.failures = 0;
 *         this.state = 'CLOSED';
 *         this.lastFailure = null;
 *     }
 *
 *     getState() {
 *         if (this.state === 'OPEN' && Date.now() - this.lastFailure > this.resetTimeout) {
 *             this.state = 'CLOSED';
 *             this.failures = 0;
 *         }
 *         return this.state;
 *     }
 *
 *     async execute(asyncFn) {
 *         if (this.getState() === 'OPEN') {
 *             throw new Error('Circuit is open');
 *         }
 *
 *         try {
 *             const result = await asyncFn();
 *             this.failures = 0;
 *             return result;
 *         } catch (error) {
 *             this.failures++;
 *             this.lastFailure = Date.now();
 *             if (this.failures >= this.threshold) {
 *                 this.state = 'OPEN';
 *             }
 *             throw error;
 *         }
 *     }
 * }
 */

/**
 * EXERCISE 11: Error Handler Chain
 *
 * Create a class 'ErrorHandler' that:
 * - Has method 'addHandler(predicate, handler)'
 * - Has async method 'handle(error)'
 * - Tries handlers in order until one matches (predicate returns true)
 * - Returns handler result if matched
 * - Re-throws if no handler matches
 */

// TODO: Implement ErrorHandler
class ErrorHandler {
  // Your code here
}

// Test
// const handler = new ErrorHandler();
// handler.addHandler(e => e.message === 'not-found', () => ({ fallback: true }));
// handler.addHandler(e => e.message === 'network', () => 'retry later');
// handler.handle(new Error('not-found')).then(r => console.log('Exercise 11:', r));

/*
 * SOLUTION 11:
 *
 * class ErrorHandler {
 *     constructor() {
 *         this.handlers = [];
 *     }
 *
 *     addHandler(predicate, handler) {
 *         this.handlers.push({ predicate, handler });
 *         return this;
 *     }
 *
 *     async handle(error) {
 *         for (const { predicate, handler } of this.handlers) {
 *             if (predicate(error)) {
 *                 return await handler(error);
 *             }
 *         }
 *         throw error;
 *     }
 * }
 */

/**
 * EXERCISE 12: Async Pipeline with Error Recovery
 *
 * Create a class 'AsyncPipeline' that:
 * - Has method 'pipe(asyncFn)' to add steps
 * - Has method 'recover(handler)' to add error recovery
 * - Has async method 'execute(input)'
 * - Runs steps in sequence, passing output to next
 * - On error, tries recovery handler, continues if it returns value
 */

// TODO: Implement AsyncPipeline
class AsyncPipeline {
  // Your code here
}

// Test
// const pipeline = new AsyncPipeline()
//     .pipe(async x => x * 2)
//     .pipe(async x => { if (x > 5) throw new Error('too big'); return x + 1; })
//     .recover(async (err) => 0)  // Recover with 0
//     .pipe(async x => x + 10);
// pipeline.execute(2).then(r => console.log('Exercise 12 (2):', r));  // 5
// pipeline.execute(5).then(r => console.log('Exercise 12 (5):', r));  // 10 (recovered)

/*
 * SOLUTION 12:
 *
 * class AsyncPipeline {
 *     constructor() {
 *         this.steps = [];
 *     }
 *
 *     pipe(asyncFn) {
 *         this.steps.push({ type: 'pipe', fn: asyncFn });
 *         return this;
 *     }
 *
 *     recover(handler) {
 *         this.steps.push({ type: 'recover', fn: handler });
 *         return this;
 *     }
 *
 *     async execute(input) {
 *         let value = input;
 *         let pendingError = null;
 *
 *         for (const step of this.steps) {
 *             if (step.type === 'pipe') {
 *                 if (pendingError) continue;
 *                 try {
 *                     value = await step.fn(value);
 *                 } catch (error) {
 *                     pendingError = error;
 *                 }
 *             } else if (step.type === 'recover') {
 *                 if (pendingError) {
 *                     try {
 *                         value = await step.fn(pendingError);
 *                         pendingError = null;
 *                     } catch (e) {
 *                         pendingError = e;
 *                     }
 *                 }
 *             }
 *         }
 *
 *         if (pendingError) throw pendingError;
 *         return value;
 *     }
 * }
 */

/**
 * EXERCISE 13: Bulkhead Pattern
 *
 * Create a class 'Bulkhead' that:
 * - Constructor takes { maxConcurrent }
 * - Has async method 'execute(asyncFn)'
 * - Limits concurrent executions to maxConcurrent
 * - Additional calls wait until slot available
 * - Prevents one failing component from affecting others
 */

// TODO: Implement Bulkhead
class Bulkhead {
  // Your code here
}

// Test
// const bulkhead = new Bulkhead({ maxConcurrent: 2 });
// const tasks = [1, 2, 3, 4].map(i =>
//     bulkhead.execute(async () => {
//         console.log(`Task ${i} started`);
//         await delay(100);
//         console.log(`Task ${i} done`);
//         return i;
//     })
// );
// Promise.all(tasks).then(r => console.log('Exercise 13:', r));

/*
 * SOLUTION 13:
 *
 * class Bulkhead {
 *     constructor({ maxConcurrent }) {
 *         this.maxConcurrent = maxConcurrent;
 *         this.running = 0;
 *         this.queue = [];
 *     }
 *
 *     async execute(asyncFn) {
 *         if (this.running >= this.maxConcurrent) {
 *             await new Promise(resolve => this.queue.push(resolve));
 *         }
 *
 *         this.running++;
 *         try {
 *             return await asyncFn();
 *         } finally {
 *             this.running--;
 *             if (this.queue.length > 0) {
 *                 const next = this.queue.shift();
 *                 next();
 *             }
 *         }
 *     }
 * }
 */

/**
 * EXERCISE 14: Error Monitoring
 *
 * Create a class 'ErrorMonitor' that:
 * - Has method 'wrap(asyncFn)' returning wrapped function
 * - Wrapped function works like original but logs errors
 * - Has method 'getStats()' returning { total, byType: {} }
 * - Tracks total errors and count by error.name
 */

// TODO: Implement ErrorMonitor
class ErrorMonitor {
  // Your code here
}

// Test
// const monitor = new ErrorMonitor();
// const riskyFn = monitor.wrap(async (shouldFail) => {
//     if (shouldFail) throw new TypeError('bad type');
//     return 'ok';
// });
// Promise.all([
//     riskyFn(false),
//     riskyFn(true).catch(() => {}),
//     riskyFn(true).catch(() => {})
// ]).then(() => console.log('Exercise 14:', monitor.getStats()));

/*
 * SOLUTION 14:
 *
 * class ErrorMonitor {
 *     constructor() {
 *         this.stats = { total: 0, byType: {} };
 *     }
 *
 *     wrap(asyncFn) {
 *         return async (...args) => {
 *             try {
 *                 return await asyncFn(...args);
 *             } catch (error) {
 *                 this.stats.total++;
 *                 const type = error.name || 'Error';
 *                 this.stats.byType[type] = (this.stats.byType[type] || 0) + 1;
 *                 throw error;
 *             }
 *         };
 *     }
 *
 *     getStats() {
 *         return { ...this.stats, byType: { ...this.stats.byType } };
 *     }
 * }
 */

/**
 * EXERCISE 15: Complete Error Handling System
 *
 * Create a class 'AsyncService' that:
 * - Constructor takes { retries, timeout, circuitThreshold }
 * - Has async method 'call(asyncFn)'
 * - Implements: timeout, retry, circuit breaker
 * - Returns { success, data, error, metadata }
 * - metadata includes: attempts, duration, circuitState
 */

// TODO: Implement AsyncService
class AsyncService {
  // Your code here
}

// Test
// const service = new AsyncService({ retries: 3, timeout: 200, circuitThreshold: 2 });
// service.call(async () => {
//     await delay(50);
//     return 'data';
// }).then(r => console.log('Exercise 15:', r));

/*
 * SOLUTION 15:
 *
 * class AsyncService {
 *     constructor({ retries, timeout, circuitThreshold }) {
 *         this.retries = retries;
 *         this.timeout = timeout;
 *         this.circuitThreshold = circuitThreshold;
 *         this.failures = 0;
 *         this.circuitOpen = false;
 *         this.lastFailure = null;
 *     }
 *
 *     async call(asyncFn) {
 *         const start = Date.now();
 *         let attempts = 0;
 *
 *         // Check circuit
 *         if (this.circuitOpen) {
 *             if (Date.now() - this.lastFailure > 5000) {
 *                 this.circuitOpen = false;
 *                 this.failures = 0;
 *             } else {
 *                 return {
 *                     success: false,
 *                     data: null,
 *                     error: new Error('Circuit open'),
 *                     metadata: {
 *                         attempts: 0,
 *                         duration: Date.now() - start,
 *                         circuitState: 'OPEN'
 *                     }
 *                 };
 *             }
 *         }
 *
 *         for (let i = 0; i < this.retries; i++) {
 *             attempts++;
 *             try {
 *                 const timeoutPromise = new Promise((_, reject) =>
 *                     setTimeout(() => reject(new Error('Timeout')), this.timeout)
 *                 );
 *                 const data = await Promise.race([asyncFn(), timeoutPromise]);
 *                 this.failures = 0;
 *
 *                 return {
 *                     success: true,
 *                     data,
 *                     error: null,
 *                     metadata: {
 *                         attempts,
 *                         duration: Date.now() - start,
 *                         circuitState: 'CLOSED'
 *                     }
 *                 };
 *             } catch (error) {
 *                 this.failures++;
 *                 this.lastFailure = Date.now();
 *
 *                 if (this.failures >= this.circuitThreshold) {
 *                     this.circuitOpen = true;
 *                 }
 *
 *                 if (i === this.retries - 1) {
 *                     return {
 *                         success: false,
 *                         data: null,
 *                         error,
 *                         metadata: {
 *                             attempts,
 *                             duration: Date.now() - start,
 *                             circuitState: this.circuitOpen ? 'OPEN' : 'CLOSED'
 *                         }
 *                     };
 *                 }
 *
 *                 await delay(100 * (i + 1));
 *             }
 *         }
 *     }
 * }
 */

// ============================================================
// RUN TESTS
// ============================================================

async function runAllTests() {
  console.log('Testing Async Error Handling Exercises...\n');
  // Uncomment individual tests above to verify solutions
}

// runAllTests();

console.log('Async Error Handling Exercises loaded.');
console.log('Uncomment tests to verify your solutions.');
Exercises - JavaScript Tutorial | DeepML