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.