Skip to main content

makeCancelable

Wraps a native Promise and allows it to be cancelled using AbortController. This is useful for cancelling long-running operations or preventing memory leaks when a component unmounts before an async operation completes. The function also provides access to the underlying AbortSignal, which can be used to coordinate cancellation across multiple promises or network requests.

API

interface MakeCancelablePromise<T = unknown> {
/**
* The wrapped promise that can be aborted
*/
promise: Promise<T>;

/**
* Aborts the promise execution. Safe to call multiple times - subsequent calls will be ignored if already cancelled.
* @param reason - Optional reason for the cancellation
*/
cancel: (reason?: any) => void;

/**
* Checks whether the promise has been cancelled
*/
isCancelled: () => boolean;

/**
* The AbortSignal object that can be used to check if the promise has been cancelled.
* This signal can be used to coordinate cancellation across multiple promises or network requests
* by passing it to other abortable operations that should be cancelled together.
*/
signal: AbortSignal;
}

function makeCancelable<T = unknown>(promise: Promise<T>): MakeCancelablePromise<T>;

Usage

import { makeCancelable, wait } from '@feedzai/js-utilities';

// A Promise that resolves after 1 second
const somePromise = wait(1000);

// Make it cancelable
const cancelable = makeCancelable(somePromise);

// Execute the wrapped promise
cancelable.promise
.then(console.log)
.catch(error => {
if (error instanceof AbortPromiseError) {
console.log('Promise was cancelled');
} else {
console.error('Other error:', error);
}
});

// Cancel it when needed
cancelable.cancel();

// Check if already cancelled
if (cancelable.isCancelled()) {
console.log('Promise was already cancelled');
}

// Use the signal with other abortable operations
fetch('/api/data', { signal: cancelable.signal })
.then(response => response.json())
.catch(error => {
if (error instanceof AbortPromiseError) {
console.log('Fetch was cancelled');
}
});

React Example

import { makeCancelable } from '@feedzai/js-utilities';
import { useEffect } from 'react';

function MyComponent() {
useEffect(() => {
const cancelable = makeCancelable(fetchData());

// Use the signal with multiple operations
const fetchUser = fetch('/api/user', { signal: cancelable.signal });
const fetchSettings = fetch('/api/settings', { signal: cancelable.signal });

Promise.all([cancelable.promise, fetchUser, fetchSettings])
.then(([data, user, settings]) => {
setData(data);
setUser(user);
setSettings(settings);
})
.catch(error => {
if (error instanceof AbortPromiseError) {
// Handle cancellation
console.log('Data fetch was cancelled');
} else {
// Handle other errors
console.error('Error fetching data:', error);
}
});

// Cleanup on unmount
return () => cancelable.cancel();
}, []);

return <div>...</div>;
}

Error Handling

When a promise is cancelled, it rejects with an AbortPromiseError. This error extends DOMException and has the following properties:

  • name: "AbortError"
  • message: "Promise was aborted"

You can check for cancellation by using instanceof:

try {
await cancelable.promise;
} catch (error) {
if (error instanceof AbortPromiseError) {
// Handle cancellation
} else {
// Handle other errors
}
}

Coordinating Multiple Operations

The signal property can be used to coordinate cancellation across multiple operations. This is particularly useful when you need to cancel multiple related operations together:

const cancelable = makeCancelable(fetchData());

// Use the same signal for multiple operations
const operation1 = new Promise((resolve, reject) => {
cancelable.signal.addEventListener('abort', () => {
reject(new AbortPromiseError());
});
// ... operation logic
});

const operation2 = new Promise((resolve, reject) => {
cancelable.signal.addEventListener('abort', () => {
reject(new AbortPromiseError());
});
// ... operation logic
});

// Cancelling the original promise will also cancel all operations using its signal
cancelable.cancel();