https://github.com/tensiile/saborter
π π© A simple and efficient library for canceling asynchronous requests using AbortController
https://github.com/tensiile/saborter
abort abortcontroller abortsignal javascript javascript-library simple ts typescript-library zero-dependency
Last synced: 23 days ago
JSON representation
π π© A simple and efficient library for canceling asynchronous requests using AbortController
- Host: GitHub
- URL: https://github.com/tensiile/saborter
- Owner: TENSIILE
- License: mit
- Created: 2025-12-28T18:29:56.000Z (2 months ago)
- Default Branch: develop
- Last Pushed: 2026-01-27T21:30:30.000Z (about 1 month ago)
- Last Synced: 2026-01-28T04:44:15.101Z (about 1 month ago)
- Topics: abort, abortcontroller, abortsignal, javascript, javascript-library, simple, ts, typescript-library, zero-dependency
- Language: TypeScript
- Homepage:
- Size: 607 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: readme.md
- Changelog: CHANGELOG.md
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
- Security: SECURITY.md
Awesome Lists containing this project
README

[](https://www.npmjs.com/package/saborter)


[](https://github.com/TENSIILE/saborter)
A simple and effective library for canceling asynchronous requests using AbortController.
## π Documentation
The documentation is divided into several sections:
- [Installation](#π¦-installation)
- [Why Saborter?](#π-why-saborter)
- [Quick Start](#π-quick-start)
- [Key Features](#π-key-features)
- [API](#π§-api)
- [Additional APIs](#π-additional-apis)
- [Important Features](#β οΈ-important-features)
- [Troubleshooting](#π-troubleshooting)
- [Usage Examples](#π―-usage-examples)
- [Compatibility](#π»-compatibility)
- [License](#π-license)
## π¦ Installation
```bash
npm install saborter
# or
yarn add saborter
```
### Related libraries
- [React](https://github.com/TENSIILE/saborter-react) - a standalone library with `Saborter` and `React` integration.
## π Why Saborter ?
| Function/Characteristic | Saborter | AbortController |
| ------------------------------------------------------------------------------------------------------------------------------------- | -------- | --------------- |
| Eliminated race condition when speed typing. | βοΈ | βοΈ |
| The signal is created anew, there is no need to recreate it yourself. After `abort()` you can "reset" and use it again. | βοΈ | βοΈ |
| Legible error handling across all browsers. | βοΈ | βοΈ |
| There is extended information about request interruptions: who cancelled, when, and the reason. | βοΈ | βοΈ |
| The signal will always be new. It's no coincidence that a previously disabled signal can appear from outside, which breaks all logic. | βοΈ | βοΈ |
## π Quick Start
### Basic Usage
```javascript
import { Aborter } from 'saborter';
// Create an Aborter instance
const aborter = new Aborter();
// Use for the request
const fetchData = async () => {
try {
const result = await aborter.try((signal) => fetch('/api/data', { signal }));
console.log('Data received:', result);
} catch (error) {
console.error('Request error:', error);
}
};
```
## π Key Features
### 1. Automatically canceling previous requests
Each time `try()` is called, the previous request is automatically canceled:
```javascript
// When searching with autocomplete
const handleSearch = async (query) => {
// The previous request is automatically canceled
const results = await aborter.try((signal) => fetch(`/api/search?q=${query}`, { signal }));
return results;
};
// When the user quickly types:
handleSearch('a'); // Starts
handleSearch('ab'); // The first request is canceled, a new one is started
handleSearch('abc'); // The second request is canceled, a new one is started
```
### 2. Automatic cancellation of requests
The `Aborter` class allows you to easily cancel ongoing requests:
```javascript
const aborter = new Aborter();
const fetcher = (signal) => fetch('/api/long-task', { signal });
// Start a long-running request and cancel the request after 2 seconds
const longRequest = aborter.try(fetcher, { timeout: { ms: 2000 } });
```
### 3. Working with Multiple Requests
You can create separate instances for different groups of requests:
```javascript
// Separate requests by type
const userAborter = new Aborter();
const dataAborter = new Aborter();
// Manage user requests separately
const fetchUser = async (id) => {
return userAborter.try((signal) => fetch(`/api/users/${id}`, { signal }));
};
// And manage data separately
const fetchData = async (params) => {
return dataAborter.try((signal) => fetch('/api/data', { signal, ...params }));
};
// Cancel only user requests
const cancelUserRequests = () => {
userAborter.abort();
};
```
## π§ API
### Constructor
```typescript
const aborter = new Aborter(options?: AborterOptions);
```
### Constructor Parameters
| Parameter | Type | Description | Required |
| --------- | ---------------- | ----------------------------- | -------- |
| `options` | `AborterOptions` | Aborter configuration options | No |
**AborterOptions:**
```typescript
{
/*
Callback function for abort events.
Associated with EventListener.onabort.
It can be overridden via `aborter.listeners.onabort`
*/
onAbort?: OnAbortCallback;
/*
A function called when the request state changes.
It takes the new state as an argument.
Can be overridden via `aborter.listeners.state.onstatechange`
*/
onStateChange?: OnStateChangeCallback;
}
```
### Properties
β οΈ `[DEPRECATED] signal`
Returns the `AbortSignal` associated with the current controller.
> [!WARNING]
> It's best not to use a signal to subscribe to interrupts or check whether a request has been interrupted.
> The signal is updated on every attempt, and your subscriptions will be lost, causing a memory leak.
```javascript
const aborter = new Aborter();
// Using signal in the request
fetch('/api/data', {
signal: aborter.signal
});
```
`isAborted`
Returns a `boolean` value indicating whether the request was aborted or not.
`listeners`
Returns an `EventListener` object to listen for `Aborter` events.
[Detailed documentation here](./docs/event-listener.md)
β οΈ `[DEPRECATED] static errorName`
Use `AbortError.name`.
Name of the `AbortError` error instance thrown by AbortSignal.
```javascript
const result = await aborter
.try((signal) => fetch('/api/data', { signal }), { isErrorNativeBehavior: true })
.catch((error) => {
if (error.name === AbortError.name) {
console.log('Canceled');
}
});
```
### Methods
`try(request, options?)`
Executes an asynchronous request with the ability to cancel.
**Parameters:**
- `request: (signal: AbortSignal) => Promise` - the function that fulfills the request
- `options?: Object` (optional)
- `isErrorNativeBehavior?: boolean` - a flag for controlling error handling. Default is `false`
- `timeout?: Object`
- `ms: number` - Time in milliseconds after which interrupts should be started
- `reason?: any` - A field storing the error reason. Can contain any metadata
**Returns:** `Promise`
**Examples:**
```javascript
// Simple request
const result = await aborter.try((signal) => {
return fetch('/api/data', { signal }).then((response) => response.json());
});
// With custom request logic
const result = await aborter.try(async (signal) => {
const response = await fetch('/api/data', { signal });
if (!response.ok) {
throw new Error('Server Error');
}
return response.json();
});
```
**Examples using automatic cancellation after a time:**
```javascript
// Request with automatic cancellation after 2 seconds
const result = await aborter.try(
(signal) => {
return fetch('/api/data', { signal });
},
{ timeout: { ms: 2000 } }
);
// We want to get an error in the "catch" block
try {
const result = await aborter.try(
(signal) => {
return fetch('/api/data', { signal });
},
{ timeout: { ms: 2000 } }
);
} catch (error) {
if (error instanceof AbortError && error.initiator === 'timeout') {
// We'll get an AbortError error here with a timeout reason.
if (error.cause instanceof TimeoutError) {
// To get the parameters that caused the timeout error,
// they can be found in the "cause" field using the upstream typeguard.
console.log(error.cause.ms); // `error.cause` β TimeoutError
}
}
}
```
If you want to catch a [timeout error through events or subscriptions](./docs/event-listener.md#catching-an-error-by-timeout), you can do that.
`abort(reason?)`
**Parameters:**
- `reason?: any` - the reason for aborting the request (optional)
Immediately cancels the currently executing request.
**Examples:**
```javascript
// Start the request
const requestPromise = aborter.try((signal) => fetch('/api/data', { signal }), { isErrorNativeBehavior: true });
// Handle cancellation
requestPromise.catch((error) => {
if (error.name === 'AbortError') {
console.log('Request canceled');
}
});
// Cancel
aborter.abort();
```
You can specify any data as the `reason`.
If we want to pass an `object`, we can put the error message in the `message` field. The same object will be available as the `reason` in the case of the `AbortError` error.
```javascript
// Start the request
const requestPromise = aborter.try((signal) => fetch('/api/data', { signal }));
// Handle cancellation
requestPromise.catch((error) => {
if (error instanceof AbortError) {
console.log(error.message); // Hello
console.log(error.reason); // { message: 'Hello', data: [] }
}
});
// Cancel
aborter.abort({ message: 'Hello', data: [] });
```
You can also submit your own `AbortError` with your own settings.
> [!WARNING]
> Please be careful, the behavior of the `Aborter` function may be broken in its original form when reconfiguring the options.
```javascript
// Start the request
const requestPromise = aborter.try((signal) => fetch('/api/data', { signal }));
// Handle cancellation
requestPromise.catch((error) => {
if (error instanceof AbortError) {
console.log(error.message); // 'Custom AbortError message'
console.log(error.reason); // 1
}
});
// Cancel
aborter.abort(new AbortError('Custom AbortError message', { reason: 1 }));
```
`abortWithRecovery(reason?)`
Immediately cancels the currently executing request.
After aborting, it restores the `AbortSignal`, resetting the `isAborted` property, and interaction with the `signal` property becomes available again.
**Parameters:**
- `reason?: any` - the reason for aborting the request (optional)
**Returns:** `AbortController`
**Examples:**
```javascript
// Create an Aborter instance
const aborter = new Aborter();
// Data retrieval function
const fetchData = async () => {
try {
const data = await fetch('/api/data', { signal: aborter.signal });
} catch (error) {
// ALL errors, including cancellations, will go here
console.log(error);
}
};
// Calling a function with a request
fetchData();
// We interrupt the request and then restore the signal
aborter.abortWithRecovery();
// Call the function again
fetchData();
```
`dispose()`
**Returns:** `void`
Clears the object's data completely: all subscriptions in all properties, clears overridden methods, state values.
> [!WARNING]
> The request does not interrupt!
`static isError(error)`
Static method for checking if an object is an `AbortError` error.
> [!IMPORTANT]
>
> - The method will return `true` even if it receives a native AbortError that is thrown by the `DOMException` itself, or finds a hint of a request abort in the error message.
> - To exclusively verify that the error is an `AbortError` from the `saborter` package, it is better to use: `error instance AbortError`
```javascript
try {
await aborter.try((signal) => fetch('/api/data', { signal }), { isErrorNativeBehavior: true });
} catch (error) {
if (Aborter.isError(error)) {
console.log('This is a cancellation error');
} else {
console.log('Another error:', error);
}
}
// or
try {
await aborter.try((signal) => fetch('/api/data', { signal }), { isErrorNativeBehavior: true });
} catch (error) {
if (error instanceof AbortError) {
console.log('This is a cancellation error');
} else {
console.log('Another error:', error);
}
}
```
## π Additional APIs
- [**AbortError**](./docs/abort-error.md) - Custom error for working with Aborter.
- [**TimeoutError**](./docs/timeout-error.md) - Error for working with timeout interrupt.
## β οΈ Important Features
### Error Handling
By default, the `try()` method does not reject the promise on `AbortError` (cancellation error). This prevents the `catch` block from being called when the request is canceled.
If you want the default behavior (the promise to be rejected on any error), use the `isErrorNativeBehavior` option:
```javascript
// The promise will be rejected even if an AbortError occurs
const result = await aborter
.try((signal) => fetch('/api/data', { signal }), { isErrorNativeBehavior: true })
.catch((error) => {
// ALL errors, including cancellations, will go here
if (error.name === 'AbortError') {
console.log('Cancelled');
}
});
```
### Resource Cleanup
Always abort requests when unmounting components or closing pages:
```javascript
// In React
useEffect(() => {
const aborter = new Aborter();
// Make requests
return () => {
aborter.abort(); // Clean up on unmount
};
}, []);
```
### Finally block
Ignoring `AbortError` cancellation errors, the `finally` block will only be executed if other errors are received, or if an abort error or the request succeeds.
```javascript
const result = await aborter
.try((signal) => fetch('/api/data', { signal }))
.catch((error) => {
// Any error other than a request cancellation will be logged here.
console.log(error);
})
.finally(() => {
// The request was successfully completed or we caught a "throw"
});
```
Everything will also work if you use the `try-catch` syntax.
```javascript
try {
const result = await aborter.try((signal) => fetch('/api/data', { signal }));
} catch (error) {
// Any error other than a request cancellation will be logged here.
console.log(error);
} finally {
// The request was successfully completed or we caught a "throw"
}
```
> [!WARNING]
> With the `isErrorNativeBehavior` flag enabled, the `finally` block will also be executed.
## π Troubleshooting
### Finally block
Many people have probably encountered the problem with the `finally` block and the classic `AbortController`. When a request is canceled, the `catch` block is called. Why would `finally` block be called? This behavior only gets in the way and causes problems.
**Example:**
```javascript
const abortController = new AbortController();
const handleLoad = async () => {
try {
setLoading(true);
const users = await fetch('/api/users', { signal: abortController.signal });
setUsers(users);
} catch (error) {
if (error.name === 'AbortError') {
console.log('interrupt error handling');
}
console.log(error);
} finally {
if (abortController.signal.aborted) {
setLoading(false);
}
}
};
const abortLoad = () => abortController.abort();
```
The problem is obvious: checking the error by name, checking the condition to see if the `AbortController` was actually terminated in the `finally` blockβit's all rather inconvenient.
How `Aborter` solves these problems:
```javascript
const aborter = new Aborter();
const handleLoad = async () => {
try {
setLoading(true);
const users = await aborter.try(getUsers);
setUsers(users);
} catch (error) {
if (error instanceof AbortError) return;
console.log(error);
} finally {
setLoading(false);
}
};
const abortLoad = () => aborter.abort();
```
The name check is gone, replaced by an instance check. It's easy to make a typo in the error name and not be able to fix it. With `instanceof` this problem disappears.
With the `finally` block, everything has become even simpler. The condition that checked for termination is completely gone.
> [!NOTE]
> If you do not use the `abort()` method to terminate a request, then the check for `AbortError` in the `catch` block can be excluded.
**Example:**
```javascript
const aborter = new Aborter();
const handleLoad = async () => {
try {
setLoading(true);
const users = await aborter.try(getUsers);
setUsers(users);
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
};
```
### Subsequent calls to the `try` method
If you want to cancel a group of requests combined in `Promise.all` or `Promise.allSettled` from a single `Aborter` instance, do not use multiple sequentially called `try` methods:
```javascript
// βοΈ Bad solution
const fetchData = async () => {
const [users, posts] = await Promise.all([
aborter.try((signal) => axios.get('/api/users', { signal })),
aborter.try((signal) => axios.get('/api/posts', { signal }))
]);
};
```
```javascript
// βοΈ Good solution
const fetchData = async () => {
const [users, posts] = await aborter.try((signal) => {
return Promise.all([axios.get('/api/users', { signal }), axios.get('/api/posts', { signal })]);
});
};
```
In the case of the first solution, the second call to the `try` method will cancel the request of the first call, which will break your logic.
## π― Usage Examples
### Example 1: Canceling multiple simultaneous requests
```javascript
const aborter = new Aborter();
const getCategoriesByUserId = async (userId) => {
const data = await aborter.try(async (signal) => {
const user = await fetch(`/api/users/${userId}`, { signal });
const categories = await fetch(`/api/categories/${user.categoryId}`, { signal });
return [await user.json(), await categories.json()];
});
return data;
};
```
### Example 2: Autocomplete
```javascript
class SearchAutocomplete {
aborter = new Aborter();
search = async (query) => {
if (!query.trim()) return [];
try {
const results = await this.aborter.try(async (signal) => {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal });
return response.json();
});
this.displayResults(results);
} catch (error) {
// Get any error except AbortError
console.error('Search error:', error);
}
};
displayResults = (results) => {
// Display the results
};
}
```
### Example 3: File Upload with Cancellation
```javascript
class FileUploader {
constructor() {
this.aborter = new Aborter();
this.progress = 0;
}
uploadFile = async (file) => {
const formData = new FormData();
formData.append('file', file);
try {
await this.aborter.try(async (signal) => {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
signal
});
// Track progress
const reader = response.body.getReader();
let receivedLength = 0;
const contentLength = +response.headers.get('Content-Length');
while (true) {
const { done, value } = await reader.read();
if (done) break;
receivedLength += value.length;
this.progress = Math.round((receivedLength / contentLength) * 100);
}
});
console.log('File uploaded successfully');
} catch (error) {
if (Aborter.isError(error)) {
console.log('Upload canceled');
} else {
console.error('Upload error:', error);
}
}
};
cancelUpload = () => {
this.aborter.abort();
};
}
```
### Example 4: Integration with UI Frameworks
**React**
```javascript
import React, { useState, useEffect, useRef } from 'react';
import { Aborter } from 'saborter';
const DataFetcher = ({ url }) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const aborterRef = useRef(new Aborter());
useEffect(() => {
return () => {
aborterRef.current.abort();
};
}, []);
const fetchData = async () => {
setLoading(true);
try {
const result = await aborterRef.current.try(async (signal) => {
const response = await fetch(url, { signal });
return response.json();
});
setData(result);
} catch (error) {
// Handle fetch error
} finally {
setLoading(false);
}
};
const cancelRequest = () => {
aborterRef.current?.abort();
};
return (
{loading ? 'Loading...' : 'Load data'}
Cancel
{data && {JSON.stringify(data, null, 2)}}
);
};
```
**Vue.js**
```javascript
import { Aborter } from 'saborter';
export default {
data() {
return {
aborter: null,
data: null,
loading: false
};
},
created() {
this.aborter = new Aborter();
},
beforeDestroy() {
this.aborter.abort();
},
methods: {
async fetchData() {
this.loading = true;
try {
this.data = await this.aborter.try(async (signal) => {
const response = await fetch(this.url, { signal });
return response.json();
});
} catch (error) {
// Handle fetch errors
} finally {
this.loading = false;
}
},
cancelRequest() {
this.aborter.abort();
}
}
};
```
## π» Compatibility
- **Browsers:** All modern browsers that support AbortController
- **Node.js:** Requires a polyfill for AbortController (version 16+ has built-in support)
- **TypeScript:** Full type support
## π License
MIT License - see [LICENSE](./LICENSE) for details.