ErrorBoundary
A class for creating error boundaries that isolate and handle errors in reactive effects and watchers.
Quick Start (30 seconds)
// Create error boundary
const boundary = new ErrorBoundary({
onError: (error, context) => {
console.error('Caught error:', error.message);
},
retry: true,
maxRetries: 3
});
// Wrap a function that might fail
const safeFetch = boundary.wrap(async () => {
const response = await fetch('/api/data');
return response.json();
}, { type: 'fetch' });
// Call it - errors are caught and handled
const result = await safeFetch();The magic: ErrorBoundary isolates errors so one failing function doesn't crash your entire application!
What is ErrorBoundary?
ErrorBoundary is a class that creates error boundaries for wrapping functions with automatic error handling, retry logic, and fallback values.
Simply put: It's a safety net that catches errors from risky functions.
Think of it like this:
- You have functions that might fail (API calls, parsing, etc.)
- Without error boundaries, one error can break everything
- With error boundaries, errors are caught and handled gracefully
- Your app continues running even when things go wrong
Syntax
// Access the class
const ErrorBoundary = ReactiveUtils.ErrorBoundary;
// Create instance
const boundary = new ErrorBoundary(options);
// Wrap functions
const safeFunction = boundary.wrap(riskyFunction, context);Parameters:
options(optional) - Configuration object for the boundary
Returns:
- A new ErrorBoundary instance with
wrap()method
Why Does This Exist?
The Challenge Without Error Boundaries
When errors occur in reactive code, they can break the entire system:
const state = ReactiveUtils.state({ count: 0, data: null });
// Risky effect - might throw error
effect(() => {
// What if this fails?
const parsed = JSON.parse(state.data);
console.log('Parsed:', parsed);
});
// TypeError: Cannot read property 'parse' of null
// 💥 Effect crashes, stops running forever
// Another effect that's perfectly fine
effect(() => {
console.log('Count:', state.count);
});
// This effect also stops! 💥
// State updates cause cascading failures
state.count = 5; // Nothing happens anymoreAt first glance, it might seem like just one error. But the consequences ripple.
What's the Real Issue?
Effect 1 runs
↓
Throws error
↓
Effect system crashes
↓
All other effects stop
↓
Entire reactivity breaks 💥
↓
App becomes unresponsiveProblems: ❌ One error breaks all effects
❌ No error isolation
❌ No retry mechanism
❌ No graceful degradation
❌ Hard to recover from errors
❌ User experience suffers
The Solution with ErrorBoundary
With ErrorBoundary, errors are isolated and handled:
const state = ReactiveUtils.state({ count: 0, data: null });
// Create error boundary
const boundary = new ErrorBoundary({
onError: (error, context) => {
console.error('Effect error:', error.message);
},
fallback: () => {
return { default: 'value' };
}
});
// Wrap risky effect
effect(boundary.wrap(() => {
const parsed = JSON.parse(state.data);
console.log('Parsed:', parsed);
}, { type: 'parse' }));
// Error caught! Logs: Effect error: Cannot read property...
// Returns fallback value
// Other effects continue working!
effect(() => {
console.log('Count:', state.count);
});
// Still works! ✓
state.count = 5;
// Logs: Count: 5 ✓What just happened?
Effect 1 runs
↓
Throws error
↓
ErrorBoundary catches it
↓
Calls onError handler
↓
Returns fallback value
↓
Other effects continue normally ✨
↓
App stays responsiveBenefits: ✅ Errors are isolated
✅ Other effects keep running
✅ Automatic retry logic available
✅ Fallback values for graceful degradation
✅ Detailed error context
✅ App remains stable
Mental Model
Think of ErrorBoundary like circuit breakers in a house:
Without ErrorBoundary (No Circuit Breakers)
House Electrical System
├─ Kitchen appliance shorts out ⚡
↓
Entire house loses power 💥
├─ Living room (no power)
├─ Bedroom (no power)
└─ Bathroom (no power)
One problem = everything stops!With ErrorBoundary (Circuit Breakers)
House Electrical System
├─ Kitchen appliance shorts out ⚡
↓
Kitchen circuit breaker trips 🔌
↓
✓ Kitchen isolated (safe)
├─ ✓ Living room (still powered)
├─ ✓ Bedroom (still powered)
└─ ✓ Bathroom (still powered)
One problem = isolated safely! ✨Key insight: Just like circuit breakers prevent one electrical problem from shutting down your entire house, ErrorBoundary prevents one code error from crashing your entire app.
How Does It Work?
Under the Hood
The ErrorBoundary class wraps functions in try-catch blocks with additional features:
// Simplified implementation
class ErrorBoundary {
constructor(options = {}) {
this.onError = options.onError || ((error) => {
console.error('Error:', error);
});
this.fallback = options.fallback;
this.retry = options.retry !== false; // Default true
this.maxRetries = options.maxRetries || 3;
this.retryDelay = options.retryDelay || 0;
}
wrap(fn, context = {}) {
let retries = 0;
return (...args) => {
const attempt = () => {
try {
return fn(...args);
} catch (error) {
retries++;
const shouldRetry = this.retry && retries < this.maxRetries;
// Call error handler
this.onError(error, {
...context,
attempt: retries,
maxRetries: this.maxRetries,
willRetry: shouldRetry
});
// Retry if configured
if (shouldRetry) {
if (this.retryDelay > 0) {
setTimeout(attempt, this.retryDelay);
} else {
return attempt();
}
} else if (this.fallback) {
return this.fallback(error, context);
}
}
};
return attempt();
};
}
}What's happening:
1️⃣ Create ErrorBoundary with options
↓
2️⃣ Wrap risky function
↓
3️⃣ Call wrapped function
↓
4️⃣ Try to execute
↓
5️⃣ If error: catch it
↓
6️⃣ Call onError handler
↓
7️⃣ Retry if configured
↓
8️⃣ Or return fallback
↓
9️⃣ Function continues safely ✨Basic Usage
Example 1: Simple Error Boundary
// Create error boundary
const boundary = new ErrorBoundary({
onError: (error) => {
console.error('Caught error:', error.message);
}
});
// Wrap a function that might fail
const riskyFunction = boundary.wrap(() => {
throw new Error('Something went wrong!');
});
// Call it - error is caught
riskyFunction();
// Logs: Caught error: Something went wrong!
// Function returns undefined (no fallback)What's happening?
- Create boundary with error handler
- Wrap function that throws error
- Call wrapped function
- Error is caught and logged
- Function returns gracefully
Example 2: With Fallback Value
const boundary = new ErrorBoundary({
onError: (error) => {
console.error('Error:', error.message);
},
fallback: () => {
return { status: 'error', data: null };
}
});
const fetchData = boundary.wrap(async () => {
const response = await fetch('/api/nonexistent');
if (!response.ok) {
throw new Error('Failed to fetch');
}
return response.json();
});
const result = await fetchData();
// Logs: Error: Failed to fetch
console.log(result);
// { status: 'error', data: null }What's happening?
- Function throws error
- Error is caught and logged
- Fallback function returns safe default value
- App continues with fallback data
Example 3: With Retry Logic
let attemptCount = 0;
const boundary = new ErrorBoundary({
retry: true,
maxRetries: 3,
onError: (error, context) => {
console.log(`Attempt ${context.attempt}/${context.maxRetries}`);
console.log('Will retry:', context.willRetry);
}
});
const flaky = boundary.wrap(() => {
attemptCount++;
console.log('Attempt number:', attemptCount);
if (attemptCount < 3) {
throw new Error('Still failing...');
}
return 'Success!';
});
const result = flaky();
// Logs:
// Attempt number: 1
// Attempt 1/3
// Will retry: true
// Attempt number: 2
// Attempt 2/3
// Will retry: true
// Attempt number: 3
console.log(result); // Success!What's happening?
- First two attempts fail
- ErrorBoundary retries automatically
- Third attempt succeeds
- Result is returned
Constructor Options
Option: onError
Error handler callback:
const boundary = new ErrorBoundary({
onError: (error, context) => {
console.error('Error occurred:', error.message);
console.log('Context:', context);
// Send to error tracking service
sendToSentry(error, context);
}
});Parameters:
error- The error object that was throwncontext- Context object with error details
Context object:
{
type: 'fetch', // Custom context from wrap()
attempt: 2, // Current attempt number
maxRetries: 3, // Maximum retries configured
willRetry: true // Whether another retry will happen
}Option: fallback
Fallback value function:
const boundary = new ErrorBoundary({
fallback: (error, context) => {
if (context.type === 'parse') {
return { default: 'value' };
}
if (context.type === 'fetch') {
return { cached: true, data: [] };
}
return null;
}
});Parameters:
error- The error objectcontext- Context from wrap()
Returns:
- Value to return instead of the error
Option: retry
Enable/disable retry logic:
// Retry enabled (default)
const boundary1 = new ErrorBoundary({
retry: true
});
// Retry disabled
const boundary2 = new ErrorBoundary({
retry: false,
fallback: () => 'default'
});Default: true
Option: maxRetries
Maximum number of retry attempts:
const boundary = new ErrorBoundary({
retry: true,
maxRetries: 5, // Try up to 5 times
onError: (error, context) => {
console.log(`Attempt ${context.attempt}/${context.maxRetries}`);
}
});Default: 3
Option: retryDelay
Delay between retry attempts (milliseconds):
const boundary = new ErrorBoundary({
retry: true,
maxRetries: 3,
retryDelay: 1000, // Wait 1 second between retries
onError: (error, context) => {
console.log('Waiting before retry...');
}
});Default: 0 (immediate retry)
Instance Methods
wrap(fn, context)
Wraps a function with error handling:
const boundary = new ErrorBoundary({ /* options */ });
const safeFunction = boundary.wrap(
() => {
// Your code here
},
{ type: 'custom', operation: 'parse' }
);Parameters:
fn- Function to wrap (can be sync or async)context- Optional context object for error tracking
Returns:
- Wrapped function that catches errors
See detailed documentation: wrap() method
Common Patterns
Pattern 1: Shared Boundary for Effects
// Create one boundary for all effects
const effectBoundary = new ErrorBoundary({
onError: (error, context) => {
console.error(`[Effect ${context.name}] Error:`, error.message);
},
fallback: () => undefined
});
const state = ReactiveUtils.state({ count: 0, name: 'Alice', data: null });
// Wrap all effects with same boundary
effect(effectBoundary.wrap(() => {
console.log('Count:', state.count);
}, { name: 'count-logger' }));
effect(effectBoundary.wrap(() => {
const parsed = JSON.parse(state.data); // Might fail
console.log('Parsed:', parsed);
}, { name: 'data-parser' }));
effect(effectBoundary.wrap(() => {
console.log('Name length:', state.name.length);
}, { name: 'name-length' }));
// If one fails, others continue
state.data = 'invalid json'; // Parse error caught, others work
state.count = 5; // All effects run (except broken one)Pattern 2: Separate Boundaries for Different Operations
// API boundary - with retry
const apiBoundary = new ErrorBoundary({
retry: true,
maxRetries: 3,
retryDelay: 1000,
onError: (error, context) => {
console.error('API Error:', error.message);
trackError('api', error);
}
});
// Parse boundary - no retry, with fallback
const parseBoundary = new ErrorBoundary({
retry: false,
fallback: () => ({ error: 'Invalid data' }),
onError: (error, context) => {
console.error('Parse Error:', error.message);
trackError('parse', error);
}
});
// Use appropriate boundary for each operation
const fetchUser = apiBoundary.wrap(async () => {
const response = await fetch('/api/user');
return response.json();
}, { type: 'fetch' });
const parseConfig = parseBoundary.wrap((configString) => {
return JSON.parse(configString);
}, { type: 'parse' });Pattern 3: Error Boundary Per Component
class TodoList {
constructor() {
// Component-specific error boundary
this.boundary = new ErrorBoundary({
onError: (error, context) => {
console.error(`[TodoList] ${context.operation}:`, error.message);
this.showErrorMessage(error.message);
},
fallback: (error, context) => {
if (context.operation === 'load') {
return [];
}
return null;
}
});
this.state = state({ todos: [], loading: false });
}
async load() {
this.state.loading = true;
const safeFetch = this.boundary.wrap(async () => {
const response = await fetch('/api/todos');
return response.json();
}, { operation: 'load' });
this.state.todos = await safeFetch();
this.state.loading = false;
}
showErrorMessage(message) {
console.log(`UI: Error - ${message}`);
}
}Pattern 4: Global Error Tracking
// Global error tracker
const errorStats = {
total: 0,
byType: {}
};
const trackingBoundary = new ErrorBoundary({
onError: (error, context) => {
// Track statistics
errorStats.total++;
const type = context.type || 'unknown';
errorStats.byType[type] = (errorStats.byType[type] || 0) + 1;
// Log to console
console.error(`[${type}] Error:`, error.message);
// Send to monitoring service
if (typeof sendToMonitoring !== 'undefined') {
sendToMonitoring({
error: error.message,
stack: error.stack,
context: context,
timestamp: Date.now()
});
}
}
});
// Use throughout app
const fetchData = trackingBoundary.wrap(async () => {
// ...
}, { type: 'api', endpoint: '/data' });
const parseData = trackingBoundary.wrap((data) => {
// ...
}, { type: 'parse', format: 'json' });
// Check stats
console.log('Error stats:', errorStats);
// { total: 5, byType: { api: 3, parse: 2 } }Pattern 5: Conditional Retry Based on Error
const smartBoundary = new ErrorBoundary({
retry: true,
maxRetries: 3,
onError: (error, context) => {
// Don't retry client errors (4xx)
if (error.status >= 400 && error.status < 500) {
context.maxRetries = 0; // Stop retrying
console.log('Client error - not retrying');
}
// Do retry server errors (5xx)
if (error.status >= 500) {
console.log(`Server error - retry ${context.attempt}/${context.maxRetries}`);
}
},
fallback: (error) => {
return { error: error.message, data: null };
}
});Summary
Key Takeaways
✅ ErrorBoundary is a class for creating error isolation boundaries
✅ Catches and handles errors preventing cascading failures
✅ Supports retry logic with configurable attempts and delays
✅ Fallback values for graceful degradation
✅ Custom error handlers for logging and tracking
✅ Context tracking provides detailed error information
✅ Wrap functions with boundary.wrap(fn, context)
Quick Reference
// Create error boundary
const boundary = new ErrorBoundary({
onError: (error, context) => {
console.error('Error:', error.message, context);
},
fallback: (error, context) => {
return 'safe default value';
},
retry: true,
maxRetries: 3,
retryDelay: 1000
});
// Wrap functions
const safeFunction = boundary.wrap(
() => {
// Risky code here
},
{ type: 'operation', name: 'my-operation' }
);
// Call wrapped function - errors are caught
const result = safeFunction();One-Line Rule
Use
ErrorBoundaryto isolate errors in reactive code—one failing function won't crash your entire application, and you get automatic retry logic and fallback values.
Next Steps:
- Learn about
wrap()method in detail - Explore error handling patterns
- Read about safeEffect() for wrapped effects