Skip to content

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 logs

Metric 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 retry

Error 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 both

With 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 ​

Released under the MIT License.