javascript
exercises
exercises.js⚡javascript
/**
* ============================================================
* 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.');