boundary.wrap()
Wrap a function with error handling, retry logic, and fallback values using an ErrorBoundary instance.
Quick Start (30 seconds)
// Create boundary
const boundary = new ErrorBoundary({
onError: (error, context) => {
console.error(`[${context.type}] Error:`, error.message);
}
});
// Wrap a risky function
const safeFetch = boundary.wrap(
async () => {
const response = await fetch('/api/data');
return response.json();
},
{ type: 'api', operation: 'fetch-data' }
);
// Call wrapped function - errors are caught
const data = await safeFetch();The magic: wrap() turns any risky function into a safe version that catches errors and handles them gracefully!
What is boundary.wrap()?
boundary.wrap() is an instance method on ErrorBoundary that wraps a function with error handling, using the configuration from the boundary instance.
Simply put: It's how you protect a function from crashing your app.
Think of it like this:
- You have a function that might throw errors
- You have a boundary with error handling configured
- Call
boundary.wrap(fn)to get a safe version - The safe version catches errors using the boundary's configuration
- Your app stays stable even when things go wrong
Syntax
// Basic wrap
const safeFunction = boundary.wrap(riskyFunction);
// With context
const safeFunction = boundary.wrap(
riskyFunction,
{ type: 'api', operation: 'fetch' }
);
// Call wrapped function normally
const result = safeFunction(arg1, arg2);
// Async functions work too
const result = await safeFetch();Parameters:
fn- Function to wrap (sync or async)context(optional) - Object with context information for error tracking
Returns:
- A wrapped function with the same signature as the original
Why Does This Exist?
The Challenge Without Wrapping
When functions throw errors, they can crash your entire application:
function parseConfig(jsonString) {
// This might throw!
return JSON.parse(jsonString);
}
function loadUserData(userId) {
// This might throw!
const response = fetch(`/api/users/${userId}`);
return response.json();
}
// Using them directly is risky
const config = parseConfig(invalidJson); // 💥 Crashes app
const user = await loadUserData(999); // 💥 Crashes app
// Every call site needs try-catch
try {
const config = parseConfig(jsonString);
} catch (error) {
console.error(error);
// Handle error...
}
// Repeat everywhere! 😰At first glance, you might think "just use try-catch everywhere." But that's the problem.
What's the Real Issue?
Risky Function 1
↓
Every caller needs try-catch
↓
Risky Function 2
↓
Every caller needs try-catch
↓
Duplicate error handling everywhere 💥
↓
Easy to forget
↓
One missing try-catch = app crashProblems: ❌ Must wrap every call in try-catch
❌ Duplicate error handling code
❌ Easy to forget
❌ Inconsistent error handling
❌ Can't reuse error logic
The Solution with boundary.wrap()
Wrap once, use safely everywhere:
// Create boundary with error handling
const boundary = new ErrorBoundary({
onError: (error, context) => {
console.error(`[${context.type}] Error:`, error.message);
logToService(error, context);
},
fallback: (error, context) => {
if (context.type === 'parse') return {};
if (context.type === 'fetch') return null;
return undefined;
}
});
// Wrap functions ONCE
const parseConfig = boundary.wrap(
(jsonString) => JSON.parse(jsonString),
{ type: 'parse' }
);
const loadUserData = boundary.wrap(
async (userId) => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
},
{ type: 'fetch' }
);
// Use safely ANYWHERE - no try-catch needed! ✨
const config = parseConfig(invalidJson); // Returns {} (fallback)
const user = await loadUserData(999); // Returns null (fallback)
// Error handling is consistent and centralizedWhat just happened?
Create boundary with config
↓
Wrap risky functions once
↓
Use everywhere safely
↓
Errors caught automatically
↓
Error handler runs
↓
Fallback returned
↓
App keeps running ✨Benefits: ✅ Wrap once, use everywhere
✅ No try-catch at call sites
✅ Consistent error handling
✅ Centralized configuration
✅ Can't forget to handle errors
Mental Model
Think of boundary.wrap() like gift wrapping:
Without Wrapping (Fragile Item)
Fragile Item (Risky Function)
↓
Hand it to someone → might drop it! 💥
↓
Hand it to someone else → might drop it! 💥
↓
Must be careful every time! 😰With Wrapping (Protected Item)
Fragile Item (Risky Function)
↓
Wrap with bubble wrap (boundary.wrap)
↓
[Protected Package]
↓
Hand to anyone → safe! ✓
↓
Drop it? → bubble wrap protects it! ✓
↓
Always safe to handle! ✨Key insight: Just like bubble wrap protects a fragile item from damage, boundary.wrap() protects your function from crashing—it adds a safety layer that catches errors no matter where the function is called.
How Does It Work?
Under the Hood
wrap() returns a new function that wraps your original function in try-catch with retry logic:
// Simplified implementation
class ErrorBoundary {
// ... constructor ...
wrap(fn, context = {}) {
let retries = 0;
return (...args) => {
const attempt = () => {
try {
// Call original function
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(); // Immediate retry
}
} else if (this.fallback) {
// Return fallback
return this.fallback(error, context);
}
// Otherwise returns undefined
}
};
return attempt();
};
}
}What's happening:
1️⃣ Call boundary.wrap(fn, context)
↓
2️⃣ Return new wrapper function
↓
3️⃣ Call wrapper(...args)
↓
4️⃣ Try to call original fn(...args)
↓
5️⃣ If error: catch it
↓
6️⃣ Call onError with context
↓
7️⃣ Retry if configured
↓
8️⃣ Or return fallback
↓
9️⃣ App continues safely ✨Preserving Function Signature
The wrapped function accepts the same arguments as the original:
// Original function
function add(a, b) {
return a + b;
}
// Wrapped function
const safeAdd = boundary.wrap(add);
// Call with same arguments
const result = safeAdd(2, 3); // 5Basic Usage
Example 1: Simple Wrap
const boundary = new ErrorBoundary({
onError: (error) => {
console.error('Error:', error.message);
}
});
// Original function that throws
function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
// Wrap it
const safeDivide = boundary.wrap(divide);
// Use it - errors are caught
console.log(safeDivide(10, 2)); // 5
console.log(safeDivide(10, 0)); // undefined (error caught)
// Logs: Error: Division by zeroWhat's happening?
- Create boundary with error handler
- Wrap divide function
- First call succeeds (returns 5)
- Second call fails but is caught (returns undefined)
Example 2: With Context
const boundary = new ErrorBoundary({
onError: (error, context) => {
console.error(`[${context.operation}] ${error.message}`);
}
});
const parseJSON = boundary.wrap(
(text) => JSON.parse(text),
{ operation: 'json-parse', format: 'json' }
);
const parseXML = boundary.wrap(
(text) => {
// Fake XML parser that might fail
if (!text.includes('<')) throw new Error('Invalid XML');
return { parsed: true };
},
{ operation: 'xml-parse', format: 'xml' }
);
parseJSON('invalid');
// Logs: [json-parse] Unexpected token i in JSON...
parseXML('invalid');
// Logs: [xml-parse] Invalid XMLWhat's happening?
- Each wrapped function has unique context
- Error handler receives context
- Can differentiate between different operations
Example 3: Async Function
const boundary = new ErrorBoundary({
onError: (error, context) => {
console.error('Fetch error:', error.message);
},
fallback: () => {
return { error: true, data: null };
}
});
const fetchUser = boundary.wrap(
async (userId) => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
},
{ type: 'fetch-user' }
);
// Use with await
const user = await fetchUser(123);
if (user.error) {
console.log('Failed to load user');
} else {
console.log('User:', user.name);
}What's happening?
- Wrap async function
- Use with await as normal
- Errors are caught
- Fallback returned on failure
Deep Dive: Context Parameter
The context parameter provides information about the wrapped function for error tracking and handling.
Basic Context
const safeFn = boundary.wrap(
() => { /* ... */ },
{ type: 'operation' }
);The context is passed to onError and fallback:
onError: (error, context) => {
// context.type === 'operation'
}Rich Context for Tracking
const boundary = new ErrorBoundary({
onError: (error, context) => {
// Use context for detailed logging
console.group('Error Details');
console.log('Type:', context.type);
console.log('Operation:', context.operation);
console.log('User:', context.userId);
console.log('Attempt:', context.attempt);
console.log('Error:', error.message);
console.groupEnd();
}
});
const fetchUserData = boundary.wrap(
async (userId) => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
},
{
type: 'api',
operation: 'fetch-user-data',
endpoint: '/api/users/:id',
userId: 123 // Can include dynamic data
}
);Context for Different Fallbacks
const boundary = new ErrorBoundary({
fallback: (error, context) => {
// Different fallbacks based on context
switch (context.type) {
case 'fetch-user':
return { id: null, name: 'Guest', email: '' };
case 'fetch-list':
return [];
case 'parse-json':
return {};
case 'parse-number':
return 0;
default:
return null;
}
}
});Context for Conditional Retry
const boundary = new ErrorBoundary({
maxRetries: 3,
onError: (error, context) => {
// Stop retrying for certain errors
if (error.status === 404) {
context.maxRetries = 0; // Stop retrying
console.log('Resource not found - not retrying');
}
if (error.status >= 500) {
console.log(`Server error - retry ${context.attempt}/${context.maxRetries}`);
}
}
});Context Properties Available
In onError and fallback, context includes:
{
// Your custom context (from wrap call)
type: 'api',
operation: 'fetch',
userId: 123,
// Added by ErrorBoundary
attempt: 2, // Current attempt number (1, 2, 3, ...)
maxRetries: 3, // Max retries from boundary config
willRetry: true // Whether another retry will happen
}Sync vs Async Functions
Synchronous Functions
// Wrap sync function
const safeParse = boundary.wrap(
(text) => JSON.parse(text)
);
// Call sync
const result = safeParse('{"key":"value"}');
console.log(result); // { key: 'value' }Asynchronous Functions
// Wrap async function
const safeFetch = boundary.wrap(
async () => {
const response = await fetch('/api/data');
return response.json();
}
);
// Call with await
const result = await safeFetch();
console.log(result);Mixing Both
const boundary = new ErrorBoundary({
onError: (error, context) => {
console.error(`[${context.name}] Error:`, error.message);
}
});
// Sync function
const parseNumber = boundary.wrap(
(str) => {
const num = parseInt(str, 10);
if (isNaN(num)) throw new Error('Not a number');
return num;
},
{ name: 'parseNumber' }
);
// Async function
const fetchData = boundary.wrap(
async () => {
const response = await fetch('/api/data');
return response.json();
},
{ name: 'fetchData' }
);
// Use both
const count = parseNumber('42'); // Sync
const data = await fetchData(); // AsyncCommon Patterns
Pattern 1: Wrap All API Calls
// Create API boundary
const apiBoundary = new ErrorBoundary({
retry: true,
maxRetries: 3,
retryDelay: 1000,
onError: (error, context) => {
console.error(`API Error [${context.endpoint}]:`, error.message);
},
fallback: (error, context) => {
return {
success: false,
error: error.message,
endpoint: context.endpoint
};
}
});
// Wrap all API functions
const api = {
getUser: apiBoundary.wrap(
async (id) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
},
{ endpoint: '/api/users/:id', type: 'fetch' }
),
getPosts: apiBoundary.wrap(
async () => {
const response = await fetch('/api/posts');
return response.json();
},
{ endpoint: '/api/posts', type: 'fetch' }
),
createPost: apiBoundary.wrap(
async (data) => {
const response = await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify(data)
});
return response.json();
},
{ endpoint: '/api/posts', type: 'create' }
)
};
// Use safely
const user = await api.getUser(123);
const posts = await api.getPosts();Pattern 2: Wrap Effects
const effectBoundary = new ErrorBoundary({
onError: (error, context) => {
console.error(`Effect "${context.name}" error:`, error.message);
}
});
const state = ReactiveUtils.state({ count: 0, data: null });
// Wrap effect functions
effect(effectBoundary.wrap(
() => {
console.log('Count:', state.count);
},
{ name: 'count-logger', type: 'effect' }
));
effect(effectBoundary.wrap(
() => {
const parsed = JSON.parse(state.data); // Might fail
console.log('Parsed:', parsed);
},
{ name: 'data-parser', type: 'effect' }
));
// If one effect fails, others continue
state.count = 5; // Works ✓
state.data = 'invalid'; // Parser fails, but count-logger still worksPattern 3: Method Wrapping in Classes
class UserService {
constructor() {
this.boundary = new ErrorBoundary({
onError: (error, context) => {
console.error(`[UserService.${context.method}] Error:`, error.message);
},
fallback: () => null
});
// Wrap all methods
this.load = this.boundary.wrap(
this._load.bind(this),
{ method: 'load' }
);
this.save = this.boundary.wrap(
this._save.bind(this),
{ method: 'save' }
);
this.delete = this.boundary.wrap(
this._delete.bind(this),
{ method: 'delete' }
);
}
async _load(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
async _save(user) {
const response = await fetch(`/api/users/${user.id}`, {
method: 'PUT',
body: JSON.stringify(user)
});
return response.json();
}
async _delete(id) {
await fetch(`/api/users/${id}`, { method: 'DELETE' });
}
}
const service = new UserService();
await service.load(123); // Safe
await service.save(user); // Safe
await service.delete(123); // SafePattern 4: Higher-Order Wrapper
function createSafeAPI(boundary) {
return {
wrap: (fn, options = {}) => {
return boundary.wrap(fn, {
type: 'api',
timestamp: Date.now(),
...options
});
}
};
}
const boundary = new ErrorBoundary({
retry: true,
maxRetries: 3,
onError: (error, context) => {
console.error('API call failed:', context);
}
});
const safeAPI = createSafeAPI(boundary);
// Use the helper
const getUser = safeAPI.wrap(
async (id) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
},
{ endpoint: '/users/:id' }
);Edge Cases and Gotchas
Gotcha 1: Wrapped Function Creates New Instance
const boundary = new ErrorBoundary();
const fn = () => { throw new Error('Fail'); };
const safe1 = boundary.wrap(fn);
const safe2 = boundary.wrap(fn);
console.log(safe1 === safe2); // false
// Each wrap() call creates a NEW functionWhat's happening:
wrap()always returns a new function- Wrapping the same function twice gives two different wrapped versions
- Each has its own retry counter
Solution:
// Wrap once, reuse
const safeFn = boundary.wrap(fn);
// Use safeFn everywhereGotcha 2: Context is Copied, Not Referenced
const context = { count: 0 };
const boundary = new ErrorBoundary({
onError: (error, ctx) => {
console.log('Count:', ctx.count);
}
});
const fn = boundary.wrap(() => {
throw new Error('Fail');
}, context);
// Modify original context
context.count = 5;
fn();
// Logs: Count: 0 (not 5!)What's happening:
- Context is copied when
wrap()is called - Modifying original doesn't affect wrapped function
- Context is frozen at wrap time
Gotcha 3: Retry Counter Persists
const boundary = new ErrorBoundary({
maxRetries: 3,
onError: (error, context) => {
console.log('Attempt:', context.attempt);
}
});
let callCount = 0;
const fn = boundary.wrap(() => {
callCount++;
if (callCount <= 3) throw new Error('Fail');
return 'Success';
});
fn(); // Attempts 1, 2, 3, 4 - returns 'Success'
console.log('Call count:', callCount); // 4
fn(); // Attempts 5, 6, 7, 8 - returns 'Success'
console.log('Call count:', callCount); // 8What's happening:
- Retry counter is created once per wrap() call
- It persists across all calls to the wrapped function
- Each call continues from the last retry count
Solution:
// If you need fresh retries each call, create a new wrapper
function wrapFresh(fn) {
return () => boundary.wrap(fn)();
}Gotcha 4: Arrow Function this Binding
class MyClass {
constructor() {
this.value = 42;
// ❌ Wrong - loses `this` context
this.method = boundary.wrap(() => {
console.log(this.value); // undefined!
});
}
}
// ✓ Solution 1: Use bind
class MyClass {
constructor() {
this.value = 42;
this.method = boundary.wrap(function() {
console.log(this.value); // 42 ✓
}.bind(this));
}
}
// ✓ Solution 2: Wrap in constructor
class MyClass {
constructor() {
this.value = 42;
this.method = boundary.wrap(this._method.bind(this));
}
_method() {
console.log(this.value); // 42 ✓
}
}Summary
Key Takeaways
✅ boundary.wrap() wraps functions with error handling from the boundary
✅ Same signature - wrapped function accepts same arguments as original
✅ Works with sync and async - handles both seamlessly
✅ Context parameter provides error tracking information
✅ Returns new function each time - wrap once, use everywhere
✅ Retry counter persists across calls to same wrapped function
✅ Use for effects, API calls, parsing - anywhere errors might occur
Quick Reference
// Create boundary
const boundary = new ErrorBoundary({ /* options */ });
// Wrap function
const safeFunction = boundary.wrap(
riskyFunction,
{ type: 'operation', name: 'my-operation' }
);
// Call wrapped function
const result = safeFunction(arg1, arg2);
// Async functions
const result = await safeAsyncFunction();
// Context in handlers
onError: (error, context) => {
console.log(context.type); // 'operation'
console.log(context.name); // 'my-operation'
console.log(context.attempt); // 1, 2, 3, ...
console.log(context.maxRetries); // From boundary config
}One-Line Rule
Use
boundary.wrap(fn, context)to create a safe version of any risky function—errors are caught, retries happen automatically, and your app stays stable.
Next Steps:
- Learn about ErrorBoundary class overview
- Learn about new ErrorBoundary() constructor
- Explore error handling patterns