When building AI-powered applications, handling long-running API requests is critical for user experience. Whether you are running a real-time chat interface, processing large datasets, or building a workflow automation tool, users need the ability to cancel requests instantly. This tutorial dives deep into the AbortController API—a browser-native solution for canceling fetch requests and streaming operations—and shows you how to integrate it seamlessly with HolySheep AI for optimal performance and cost savings.
HolySheep AI vs Official API vs Other Relay Services
| Feature | HolySheep AI | Official API (OpenAI) | Other Relay Services |
|---|---|---|---|
| Rate | ¥1 = $1 (85%+ savings) | $7.30+ per $1 | $2.50-$6.00 per $1 |
| Payment Methods | WeChat, Alipay, Stripe | International cards only | Varies |
| Latency | <50ms overhead | 80-200ms (CN region) | 100-300ms |
| Free Credits | Yes, on signup | $5 trial (limited) | Rarely |
| AbortController Support | Full native support | Full native support | Inconsistent |
Why Cancel API Requests?
In my experience building production AI applications, I have encountered countless scenarios where request cancellation is not just nice-to-have—it is essential. Consider a user who starts a complex document analysis, then changes their mind and initiates a new query. Without proper cancellation, the previous request continues consuming resources and credits until completion.
The AbortController interface provides a standardized way to abort one or more Web requests. It is built into all modern browsers and Node.js 15+ environments, making it the go-to solution for:
- User-initiated cancellations: Stop button in UI, navigation away from page
- Timeout-based cancellations: Auto-abort after configurable threshold
- Race conditions: Cancel previous request when new one starts
- Resource cleanup: Ensure no orphaned requests on component unmount
How AbortController Works
The AbortController creates an AbortSignal object that can be passed to any fetch() request. When you call abort(), the signal transitions to an aborted state, and the fetch promise rejects with an AbortError.
// Basic AbortController structure
const controller = new AbortController();
const signal = controller.signal;
// Pass signal to fetch
fetch(url, { signal });
// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);
// Handle the abort
try {
const response = await fetch(url, { signal });
const data = await response.json();
console.log(data);
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request was cancelled');
}
}
Integrating AbortController with HolySheep AI
Here is a complete implementation that combines AbortController with HolySheep AI's API, featuring automatic timeout handling, streaming response support, and proper cleanup:
/**
* HolySheep AI API client with AbortController support
* Base URL: https://api.holysheep.ai/v1
*/
class HolySheepAIClient {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = 'https://api.holysheep.ai/v1';
this.activeControllers = new Map();
}
/**
* Send a chat completion request with cancellation support
* @param {Object} params - Request parameters
* @param {string} params.model - Model name (gpt-4.1, claude-sonnet-4.5, etc.)
* @param {Array} params.messages - Chat messages array
* @param {number} params.timeout - Timeout in milliseconds (default: 30000)
* @param {string} requestId - Unique identifier for this request
* @returns {Promise<Object>}
*/
async chatCompletion({ model, messages, timeout = 30000 }, requestId) {
// Create new AbortController for this request
const controller = new AbortController();
this.activeControllers.set(requestId, controller);
// Set up timeout
const timeoutId = setTimeout(() => {
controller.abort();
this.activeControllers.delete(requestId);
}, timeout);
try {
const response = await fetch(${this.baseUrl}/chat/completions, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': Bearer ${this.apiKey}
},
body: JSON.stringify({
model,
messages,
stream: false
}),
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(API Error ${response.status}: ${errorData.error?.message || response.statusText});
}
const data = await response.json();
return data;
} catch (error) {
clearTimeout(timeoutId);
this.activeControllers.delete(requestId);
if (error.name === 'AbortError') {
throw new CancellationError('Request was cancelled or timed out');
}
throw error;
}
}
/**
* Cancel a specific request
* @param {string} requestId - The request ID to cancel
*/
cancelRequest(requestId) {
const controller = this.activeControllers.get(requestId);
if (controller) {
controller.abort();
this.activeControllers.delete(requestId);
console.log(Cancelled request: ${requestId});
}
}
/**
* Cancel all active requests (cleanup on unmount)
*/
cancelAll() {
for (const [requestId, controller] of this.activeControllers) {
controller.abort();
}
this.activeControllers.clear();
console.log('Cancelled all active requests');
}
}
// Custom error class for cancellations
class CancellationError extends Error {
constructor(message) {
super(message);
this.name = 'CancellationError';
}
}
// Usage Example
async function main() {
const client = new HolySheepAIClient('YOUR_HOLYSHEEP_API_KEY');
// Generate unique request ID
const requestId = req_${Date.now()};
try {
const result = await client.chatCompletion({
model: 'gpt-4.1',
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: 'Explain quantum entanglement in simple terms.' }
],
timeout: 30000
}, requestId);
console.log('Response:', result.choices[0].message.content);
} catch (error) {
if (error instanceof CancellationError) {
console.log('Request was cancelled:', error.message);
} else {
console.error('API Error:', error.message);
}
}
}
main();
Streaming Requests with AbortController
Streaming responses (like stream: true) require special handling because the fetch promise resolves immediately while data arrives incrementally. Here is a robust implementation for streaming scenarios:
/**
* Streaming chat completion with AbortController
* Supports real-time cancellation and cleanup
*/
async function* streamChatCompletion(apiKey, messages, options = {}) {
const {
model = 'gpt-4.1',
timeout = 60000,
signal: externalSignal
} = options;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
// Combine external signal with internal controller
const mergedSignal = externalSignal
? anySignal([externalSignal, controller.signal])
: controller.signal;
let cleanup = () => {
clearTimeout(timeoutId);
controller.abort();
};
try {
const response = await fetch('https://api.holysheep.ai/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': Bearer ${apiKey}
},
body: JSON.stringify({
model,
messages,
stream: true
}),
signal: mergedSignal
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(API Error: ${error.error?.message || response.statusText});
}
if (!response.body) {
throw new Error('Response body is not available');
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
cleanup = () => {}; // Prevent double cleanup
return; // Generator complete
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
return;
}
try {
const parsed = JSON.parse(data);
if (parsed.choices?.[0]?.delta?.content) {
yield parsed.choices[0].delta.content;
}
} catch (e) {
// Skip malformed JSON
}
}
}
}
} finally {
reader.releaseLock();
}
} catch (error) {
if (error.name === 'AbortError') {
throw new CancellationError('Stream was cancelled or timed out');
}
throw error;
} finally {
cleanup();
}
}
// Utility: Combine multiple AbortSignals
function anySignal(signals) {
const controller = new AbortController();
const stop = () => controller.abort();
for (const signal of signals) {
if (signal.aborted) {
stop();
return controller.signal;
}
signal.addEventListener('abort', stop, { once: true });
}
return controller.signal;
}
// React Hook Example
function useStreamingChat(apiKey) {
const [output, setOutput] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
const abortControllerRef = useRef(null);
const startStream = async (messages) => {
// Cancel any existing stream
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
abortControllerRef.current = new AbortController();
setOutput('');
setIsStreaming(true);
try {
for await (const chunk of streamChatCompletion(apiKey, messages, {
signal: abortControllerRef.current.signal
})) {
setOutput(prev => prev + chunk);
}
} catch (error) {
if (error instanceof CancellationError) {
console.log('Stream cancelled by user');
} else {
console.error('Stream error:', error);
}
} finally {
setIsStreaming(false);
}
};
const cancelStream = () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
// Cleanup on unmount
useEffect(() => {
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, []);
return { output, isStreaming, startStream, cancelStream };
}
2026 Pricing Reference for HolySheep AI
When implementing cancellation logic, it is important to understand the cost implications. With HolySheep AI, the rate of ¥1 = $1 (saving 85%+ compared to the official ¥7.3 rate) means that cancelled requests before token generation cost nothing. Here are the current output pricing:
| Model | Output Price ($/MTok) | Input Price ($/MTok) |
|---|---|---|
| GPT-4.1 | $8.00 | $2.00 |
| Claude Sonnet 4.5 | $15.00 | $3.00 |
| Gemini 2.5 Flash | $2.50 | $0.30 |
| DeepSeek V3.2 | $0.42 | $0.14 |
Common Errors and Fixes
1. AbortError Not Being Caught
Error: Uncaught (in promise) AbortError: The user aborted a request.
Cause: The fetch promise rejects with AbortError only when the request is aborted before completion. For streaming responses that have already resolved, cancellation happens differently.
// ❌ Wrong: Not handling AbortError properly
async function badRequest() {
const response = await fetch(url, { signal: controller.signal });
return response.json(); // May reject after abort
}
// ✅ Correct: Check signal.aborted before processing
async function goodRequest() {
const response = await fetch(url, { signal: controller.signal });
// Check if aborted during fetch
if (controller.signal.aborted) {
throw new CancellationError('Request cancelled');
}
const data = await response.json();
// Double-check before returning
if (controller.signal.aborted) {
throw new CancellationError('Request cancelled during JSON parsing');
}
return data;
}
2. Memory Leaks from Uncleaned Controllers
Error: Controllers array grows indefinitely, memory usage increases over time.
Cause: AbortControllers are not garbage collected when stored in Maps without cleanup.
// ❌ Wrong: Memory leak
class LeakyClient {
constructor() {
this.controllers = new Map();
}
async request(id) {
const controller = new AbortController();
this.controllers.set(id, controller); // Never cleaned up on success
// ...
}
}
// ✅ Correct: Proper cleanup
class CleanClient {
constructor() {
this.controllers = new Map();
this.timeouts = new Map();
}
async request(id, timeout = 30000) {
const controller = new AbortController();
this.controllers.set(id, controller);
const timeoutId = setTimeout(() => {
controller.abort();
this.cleanup(id);
}, timeout);
this.timeouts.set(id, timeoutId);
try {
const result = await fetch(url, { signal: controller.signal });
this.cleanup(id); // Clean up on success
return result;
} catch (error) {
this.cleanup(id); // Clean up on any error
throw error;
}
}
cleanup(id) {
this.controllers.delete(id);
if (this.timeouts.has(id)) {
clearTimeout(this.timeouts.get(id));
this.timeouts.delete(id);
}
}
cancelAll() {
for (const controller of this.controllers.values()) {
controller.abort();
}
for (const timeoutId of this.timeouts.values()) {
clearTimeout(timeoutId);
}
this.controllers.clear();
this.timeouts.clear();
}
}
3. Race Condition: Multiple Rapid Requests
Error: Previous requests complete after newer requests, causing UI to show outdated results.
Cause: No mechanism to cancel previous requests when new ones start.
// ❌ Wrong: Race condition
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', async (e) => {
const results = await fetchResults(e.target.value); // Multiple concurrent
renderResults(results); // May render out-of-order
});
// ✅ Correct: Cancel previous request
class RequestRaceHandler {
constructor() {
this.currentController = null;
}
async search(query) {
// Cancel previous request
if (this.currentController) {
this.currentController.abort('Replaced by newer request');
}
this.currentController = new AbortController();
const signal = this.currentController.signal;
try {
const results = await fetchResults(query, signal);
// Verify this is still the latest request
if (signal.aborted) {
console.log('Request superseded by newer query');
return null;
}
return results;
} catch (error) {
if (error.name === 'AbortError') {
return null; // Expected cancellation
}
throw error;
} finally {
// Clear reference if this is the current controller
if (this.currentController === this.controller) {
this.currentController = null;
}
}
}
}
// Usage
const handler = new RequestRaceHandler();
searchInput.addEventListener('input', (e) => handler.search(e.target.value));
4. React StrictMode Double Invocation
Error: In React 18 StrictMode, effects run twice, causing duplicate API calls.
Cause: React intentionally mounts components twice in development to detect side effects.
// ❌ Wrong: Not handling StrictMode
useEffect(() => {
const controller = new AbortController();
fetchData(controller.signal).then(setData);
return () => controller.abort(); // Runs on unmount and second mount
}, [fetchKey]);
// ✅ Correct: Track if still mounted
function useDataFetch(fetchKey) {
const [data, setData] = useState(null);
const mountedRef = useRef(true);
useEffect(() => {
mountedRef.current = true;
const controller = new AbortController();
fetchData(controller.signal)
.then(result => {
if (mountedRef.current) {
setData(result);
}
})
.catch(error => {
if (error.name !== 'AbortError' && mountedRef.current) {
console.error('Fetch error:', error);
}
});
return () => {
mountedRef.current = false;
controller.abort();
};
}, [fetchKey]);
return data;
}
// ✅ Alternative: Use AbortController singleton pattern
const globalAbortController = new AbortController();
function useGlobalAbort() {
useEffect(() => {
return () => {
// Only abort if truly unmounting (not StrictMode remount)
// This requires additional logic if you need per-instance cancellation
};
}, []);
return globalAbortController.signal;
}
Best Practices Summary
- Always clean up: Delete controllers from Maps and clear timeouts in finally blocks
- Handle AbortError gracefully: Distinguish between user cancellation and errors
- Implement race condition prevention: Cancel stale requests in auto-complete scenarios
- Use reasonable timeouts: 30-60 seconds for standard requests, adjust for streaming
- Leverage HolySheep pricing: With ¥1=$1 and free credits, you can afford longer timeouts without worrying about costs
Conclusion
Mastering AbortController is essential for building responsive, resource-efficient AI applications. By implementing proper cancellation logic, you improve user experience, reduce unnecessary API costs, and prevent memory leaks. HolySheep AI provides the infrastructure to make this seamless—with sub-50ms latency, payment flexibility through WeChat and Alipay, and the most competitive rates in the market (85%+ savings).
Start implementing these patterns today and give your users the control they deserve over long-running AI operations.