Skip to content

onCancel()

Register a callback to execute when the task is cancelled.

Syntax

typescript
task.onCancel(callback: () => void): FuturableTask<T>

Parameters

callback

A function to execute when the task is cancelled. The callback receives no arguments.

Return Value

Returns this for method chaining.

Description

The onCancel() method registers a callback that will be executed when the task is cancelled via cancel(). Multiple callbacks can be registered and they will be executed in the order they were added.

Key Behaviors

  • Eager Registration: Callbacks are registered at the task level, not per execution
  • Multiple Callbacks: Can register multiple callbacks on the same task
  • Execution Order: Callbacks execute in registration order
  • Chainable: Returns this for fluent API
  • One-Time: Each callback executes once when cancelled

Examples

Basic Usage

typescript
const task = FuturableTask
  .of(() => longOperation())
  .onCancel(() => {
    console.log('Task was cancelled');
  });

task.cancel(); // Logs: "Task was cancelled"

Resource Cleanup

typescript
const task = FuturableTask.of((resolve, reject) => {
  const ws = new WebSocket('wss://example.com');
  const timer = setTimeout(() => resolve('done'), 5000);

  // Cleanup on cancellation
  task.onCancel(() => {
    clearTimeout(timer);
    ws.close();
    console.log('Resources cleaned up');
  });

  ws.onmessage = (msg) => resolve(msg.data);
});

Multiple Callbacks

typescript
const task = FuturableTask
  .of(() => fetchData())
  .onCancel(() => console.log('Cleanup 1'))
  .onCancel(() => console.log('Cleanup 2'))
  .onCancel(() => console.log('Cleanup 3'));

task.cancel();
// Logs in order:
// "Cleanup 1"
// "Cleanup 2"
// "Cleanup 3"

Method Chaining

typescript
const task = FuturableTask
  .of(() => fetchData())
  .map(data => processData(data))
  .onCancel(() => console.log('Cancelled'))
  .retry(3)
  .timeout(5000)
  .onCancel(() => console.log('All cleanup done'));

State Management

typescript
let isRunning = false;

const task = FuturableTask
  .of(() => {
    isRunning = true;
    return heavyComputation();
  })
  .onCancel(() => {
    isRunning = false;
    console.log('Computation stopped');
  });

const execution = task.run();

// Cancel and update state
setTimeout(() => task.cancel(), 1000);

UI Integration

typescript
function LoadingButton({ onClick }) {
  const [loading, setLoading] = useState(false);

  const handleClick = () => {
    const task = FuturableTask
      .of(() => performAction())
      .onCancel(() => {
        setLoading(false);
        showMessage('Action cancelled');
      });

    setLoading(true);
    task.run()
      .then(() => setLoading(false))
      .catch(() => setLoading(false));

    // Save task reference for cancellation
    window.currentTask = task;
  };

  return (
    <button onClick={handleClick} disabled={loading}>
      {loading ? 'Loading...' : 'Click Me'}
    </button>
  );
}

File Upload Cancellation

typescript
const uploadTask = FuturableTask
  .of(() => {
    const formData = new FormData();
    formData.append('file', file);
    return fetch('/upload', { method: 'POST', body: formData });
  })
  .onCancel(() => {
    console.log('Upload cancelled');
    updateProgressBar(0);
    showNotification('Upload cancelled by user');
  });

// User clicks cancel button
cancelButton.addEventListener('click', () => {
  uploadTask.cancel();
});

Database Transaction Rollback

typescript
const transaction = FuturableTask
  .of(async () => {
    await db.begin();
    await db.execute('INSERT INTO users ...');
    await db.execute('UPDATE accounts ...');
    await db.commit();
  })
  .onCancel(async () => {
    console.log('Rolling back transaction');
    await db.rollback();
  });

Network Request Abort

typescript
const controller = new AbortController();

const task = FuturableTask
  .of(() => fetch('/api/data', { signal: controller.signal }))
  .onCancel(() => {
    controller.abort();
    console.log('Network request aborted');
  });

Timer Cleanup

typescript
const task = FuturableTask.of((resolve) => {
  const timers: NodeJS.Timeout[] = [];

  timers.push(setTimeout(() => console.log('Step 1'), 1000));
  timers.push(setTimeout(() => console.log('Step 2'), 2000));
  timers.push(setTimeout(() => resolve('done'), 3000));

  // Cleanup all timers on cancel
  task.onCancel(() => {
    timers.forEach(timer => clearTimeout(timer));
    console.log('All timers cleared');
  });
});

EventSource Cleanup

typescript
const task = FuturableTask.of(() => {
  const eventSource = new EventSource('/events');

  return new Promise((resolve) => {
    eventSource.onmessage = (event) => {
      resolve(event.data);
    };
  });
}).onCancel(() => {
  eventSource.close();
  console.log('EventSource closed');
});

Combining with Executor onCancel

You can use both executor-level and task-level cancellation:

typescript
const task = new FuturableTask((resolve, reject, { onCancel }) => {
  const timer = setTimeout(() => resolve('done'), 5000);

  // Executor-level cleanup (runs per execution)
  onCancel(() => {
    clearTimeout(timer);
    console.log('Execution cancelled');
  });
});

// Task-level cleanup (runs once for the task)
task.onCancel(() => {
  console.log('Task cancelled');
});

task.cancel();
// Logs:
// "Task cancelled" (task-level)
// "Execution cancelled" (executor-level, for any running execution)

Common Patterns

Cleanup Manager

typescript
class ResourceManager {
  private resources: any[] = [];

  createTask<T>(work: () => T) {
    return FuturableTask.of(work).onCancel(() => {
      this.cleanup();
    });
  }

  addResource(resource: any) {
    this.resources.push(resource);
  }

  cleanup() {
    this.resources.forEach(r => r.dispose());
    this.resources = [];
  }
}

Progress Reset

typescript
const task = FuturableTask
  .of(() => processLargeFile())
  .onCancel(() => {
    progressBar.reset();
    statusText.innerText = 'Processing cancelled';
  });

Analytics Tracking

typescript
const task = FuturableTask
  .of(() => performOperation())
  .onCancel(() => {
    analytics.track('operation_cancelled', {
      timestamp: Date.now(),
      reason: 'user_initiated'
    });
  });

Notification Display

typescript
const task = FuturableTask
  .of(() => longProcess())
  .onCancel(() => {
    toast.show({
      message: 'Operation cancelled',
      type: 'warning',
      duration: 3000
    });
  });

Best Practices

1. Always Cleanup Resources

typescript
// ✅ Good - cleanup all resources
const task = FuturableTask
  .of(() => {
    const resource = allocate();
    return process(resource);
  })
  .onCancel(() => {
    resource.dispose();
  });

// ❌ Bad - resource leak
const task = FuturableTask.of(() => {
  const resource = allocate();
  return process(resource);
});

2. Keep Callbacks Simple

typescript
// ✅ Good - simple, focused callback
.onCancel(() => {
  cleanup();
  updateUI();
})

// ❌ Bad - complex logic
.onCancel(() => {
  if (condition1) {
    // lots of logic
  } else if (condition2) {
    // more logic
  }
  // ...
})

3. Don't Throw in Callbacks

typescript
// ✅ Good - handle errors internally
.onCancel(() => {
  try {
    riskyCleanup();
  } catch (error) {
    console.error('Cleanup failed:', error);
  }
})

// ❌ Bad - throwing errors
.onCancel(() => {
  riskyCleanup(); // May throw
})

4. Use for Side Effects Only

typescript
// ✅ Good - side effects only
.onCancel(() => {
  logCancellation();
  closeConnections();
})

// ❌ Bad - trying to change task behavior
.onCancel(() => {
  return 'cancelled'; // Ignored
})

Execution Timing

typescript
const task = FuturableTask
  .of(() => work())
  .onCancel(() => console.log('1'))
  .onCancel(() => console.log('2'));

console.log('Before cancel');
task.cancel();
console.log('After cancel');

// Output:
// "Before cancel"
// "1"
// "2"
// "After cancel"

Notes

  • Callbacks are executed synchronously when cancel() is called
  • Callbacks execute even if the task was never run
  • Return values from callbacks are ignored
  • Errors thrown in callbacks are not caught (wrap in try-catch)
  • Multiple calls to onCancel() add more callbacks
  • Callbacks are called only once per cancel() invocation
  • Works in combination with executor-level onCancel()

See Also

Released under the MIT License.