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