tapError()
Perform side effects on errors without changing the error.
Syntax
typescript
task.tapError(fn: (error: any) => void | Promise<void>): FuturableTask<T>Parameters
fn
Function to execute when the task fails. The return value is ignored.
- Input: The error that occurred
- Output: Ignored (void)
Return Value
A new FuturableTask<T> that executes the function on error but propagates the original error.
Description
The tapError() method allows you to perform side effects (like logging or analytics) when a task fails, without modifying the error or handling it. The error continues to propagate after the side effect.
This is the error equivalent of tap() for success values.
Examples
Error Logging
typescript
const task = FuturableTask
.of(() => riskyOperation())
.tapError(error => console.error('Operation failed:', error))
.retry(3);Analytics Tracking
typescript
const task = FuturableTask
.fetch('/api/data')
.tapError(error => {
analytics.track('api_error', {
endpoint: '/api/data',
error: error.message,
timestamp: Date.now()
});
});Multiple Error Handlers
typescript
const task = FuturableTask
.of(() => complexOperation())
.tapError(error => console.error('Error:', error))
.tapError(error => logger.error(error))
.tapError(error => notifyAdmins(error))
.recover(error => fallbackValue);Conditional Logging
typescript
const task = FuturableTask
.fetch('/api/data')
.tapError(error => {
if (error.status >= 500) {
console.error('Server error:', error);
alertOps(error);
}
});Use Cases
Error Reporting Service
typescript
const task = FuturableTask
.of(() => criticalOperation())
.tapError(error => {
errorReportingService.report({
error,
context: {
user: getCurrentUser(),
route: getCurrentRoute(),
timestamp: new Date()
}
});
});Debug Logging
typescript
const task = FuturableTask
.of(() => computation())
.tap(result => console.log('Success:', result))
.tapError(error => console.error('Failed:', error));
// See both success and failure in logsMetric Collection
typescript
const task = FuturableTask
.fetch('/api/expensive-operation')
.tap(() => metrics.increment('operation.success'))
.tapError(() => metrics.increment('operation.failure'));User Feedback
typescript
const task = FuturableTask
.of(() => saveDocument())
.tap(() => showToast('Document saved!'))
.tapError(error => {
showToast(`Failed to save: ${error.message}`, 'error');
});Retry with Logging
typescript
const task = FuturableTask
.of(() => unreliableAPI())
.tapError((error) => {
console.log('Attempt failed, will retry:', error);
})
.retry(3, { delay: 1000 });
// Logs each failure before retryError Categorization
typescript
const task = FuturableTask
.fetch('/api/data')
.tapError(error => {
if (error.name === 'NetworkError') {
networkErrorCount++;
} else if (error.status === 404) {
notFoundErrorCount++;
} else {
unknownErrorCount++;
}
});Combining with Other Methods
With tap()
typescript
const task = FuturableTask
.of(() => operation())
.tap(result => console.log('✅ Success:', result))
.tapError(error => console.error('❌ Failed:', error));
// Logs either success or failure, never bothWith recover()
typescript
const task = FuturableTask
.of(() => riskyOperation())
.tapError(error => {
console.error('Primary failed:', error);
logToFile(error);
})
.recover(error => {
console.log('Using fallback');
return fallbackValue;
});With orElse()
typescript
const task = FuturableTask
.fetch('/api/primary')
.tapError(error => console.log('Primary failed:', error))
.orElse(() =>
FuturableTask.fetch('/api/backup')
.tapError(error => console.log('Backup failed:', error))
)
.recover(() => DEFAULT_DATA);In a Pipeline
typescript
const pipeline = FuturableTask
.of(() => fetchData())
.tap(data => console.log('Fetched:', data.length, 'items'))
.map(data => validateData(data))
.tapError(error => console.error('Validation failed:', error))
.map(data => transformData(data))
.tapError(error => console.error('Transform failed:', error))
.flatMap(data => saveData(data))
.tapError(error => console.error('Save failed:', error));Pattern: Observability
typescript
class ObservableTask {
static wrap<T>(name: string, task: FuturableTask<T>) {
const startTime = Date.now();
return task
.tap(result => {
const duration = Date.now() - startTime;
console.log(`✅ ${name} succeeded in ${duration}ms`);
metrics.timing(`${name}.duration`, duration);
metrics.increment(`${name}.success`);
})
.tapError(error => {
const duration = Date.now() - startTime;
console.error(`❌ ${name} failed in ${duration}ms:`, error);
metrics.timing(`${name}.duration`, duration);
metrics.increment(`${name}.failure`);
});
}
}
// Usage
const task = ObservableTask.wrap(
'fetchUserData',
FuturableTask.fetch('/api/user')
);Best Practices
1. Keep Side Effects Pure
typescript
// ✅ Good - no mutation
.tapError(error => {
console.error(error);
sendToLogger(error);
})
// ❌ Bad - mutation
.tapError(error => {
error.logged = true; // Mutating error
})2. Don't Throw in tapError
typescript
// ✅ Good - catch errors
.tapError(error => {
try {
riskyLogging(error);
} catch (loggingError) {
console.error('Logging failed:', loggingError);
}
})
// ❌ Bad - can throw
.tapError(error => {
riskyLogging(error); // May throw and mask original error
})3. Use for Observation, Not Handling
typescript
// ✅ Good - observe and let error propagate
.tapError(error => console.error(error))
.recover(error => fallbackValue)
// ❌ Bad - trying to handle in tapError
.tapError(error => {
return fallbackValue; // Return value is ignored!
})4. Async Side Effects
typescript
// tapError can be async
const task = FuturableTask
.of(() => operation())
.tapError(async error => {
await saveErrorToDatabase(error);
await notifyAdmin(error);
});Comparison with recover()
typescript
// tapError - observe error, don't handle
const observe = FuturableTask
.of(() => fail())
.tapError(error => console.log(error))
// Error still propagates
// recover - handle error
const handle = FuturableTask
.of(() => fail())
.recover(error => {
console.log(error);
return 'recovered';
});
// Error is handled, returns 'recovered'Notes
- The function is only called if the task fails
- Return value is completely ignored
- Error continues to propagate unchanged
- Can be async (returns Promise<void>)
- Multiple
tapError()calls can be chained - Does not catch or handle the error
- Useful for logging, metrics, and monitoring
See Also
- tap() - Side effects on success
- orElse() - Alternative tasks
- Composition Guide - Composition patterns
