API Timeout Handling
What This Means
Without proper timeout handling, API requests can hang indefinitely, causing:
- Frozen UI elements
- Memory leaks
- Poor user experience
- Wasted resources on failed requests
How to Diagnose
Identify Hanging Requests
DevTools > Network:
- Look for requests with "Pending" status for long periods
- Check for requests without response
- Filter by time to find slow requests
Console Monitoring
// Log slow requests
const originalFetch = window.fetch;
window.fetch = async function(...args) {
const start = performance.now();
try {
const response = await originalFetch.apply(this, args);
const duration = performance.now() - start;
if (duration > 3000) {
console.warn(`Slow request (${duration}ms):`, args[0]);
}
return response;
} catch (error) {
console.error('Request failed:', args[0], error);
throw error;
}
};
General Fixes
Fetch with AbortController
// Basic timeout implementation
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`Request timed out after ${timeout}ms`);
}
throw error;
}
}
// Usage
try {
const response = await fetchWithTimeout('/api/data', {}, 5000);
const data = await response.json();
} catch (error) {
if (error.message.includes('timed out')) {
showTimeoutMessage();
}
}
Configurable Timeout Utility
class ApiClient {
constructor(baseUrl, defaultTimeout = 10000) {
this.baseUrl = baseUrl;
this.defaultTimeout = defaultTimeout;
}
async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const timeout = options.timeout || this.defaultTimeout;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const config = {
...options,
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
...options.headers
}
};
try {
const response = await fetch(url, config);
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new TimeoutError(url, timeout);
}
throw error;
}
}
get(endpoint, options) {
return this.request(endpoint, { ...options, method: 'GET' });
}
post(endpoint, data, options) {
return this.request(endpoint, {
...options,
method: 'POST',
body: JSON.stringify(data)
});
}
}
class TimeoutError extends Error {
constructor(url, timeout) {
super(`Request to ${url} timed out after ${timeout}ms`);
this.name = 'TimeoutError';
this.url = url;
this.timeout = timeout;
}
}
// Usage
const api = new ApiClient('https://api.example.com', 5000);
const data = await api.get('/users', { timeout: 3000 });
Axios Timeout
import axios from 'axios';
const api = axios.create({
baseURL: 'https://api.example.com',
timeout: 5000, // 5 seconds
timeoutErrorMessage: 'Request timed out'
});
// Per-request timeout
const response = await api.get('/users', { timeout: 3000 });
// Handle timeout errors
api.interceptors.response.use(
response => response,
error => {
if (error.code === 'ECONNABORTED') {
// Timeout error
return Promise.reject(new Error('Request timed out'));
}
return Promise.reject(error);
}
);
React Query with Timeout
import { useQuery } from '@tanstack/react-query';
async function fetchWithTimeout(url) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
return response.json();
}
function UserData() {
const { data, error, isLoading } = useQuery({
queryKey: ['users'],
queryFn: () => fetchWithTimeout('/api/users'),
retry: (failureCount, error) => {
// Don't retry on timeout
if (error.name === 'AbortError') return false;
return failureCount < 3;
}
});
if (isLoading) return <Loading />;
if (error) return <Error message={error.message} />;
return <UserList users={data} />;
}
Progressive Timeout Strategy
// Increase timeout for retries
async function fetchWithProgressiveTimeout(url, options = {}) {
const timeouts = [2000, 5000, 10000]; // Progressive timeouts
let lastError;
for (let i = 0; i < timeouts.length; i++) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeouts[i]);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
lastError = error;
if (error.name === 'AbortError') {
console.log(`Attempt ${i + 1} timed out after ${timeouts[i]}ms`);
continue; // Try next timeout
}
throw error; // Non-timeout error, don't retry
}
}
throw new Error(`All attempts failed. Last error: ${lastError.message}`);
}
UI Feedback for Timeouts
function LoadingWithTimeout({ timeout = 5000, onTimeout }) {
const [showSlowMessage, setShowSlowMessage] = useState(false);
useEffect(() => {
const timer = setTimeout(() => {
setShowSlowMessage(true);
}, timeout / 2);
const timeoutTimer = setTimeout(() => {
onTimeout?.();
}, timeout);
return () => {
clearTimeout(timer);
clearTimeout(timeoutTimer);
};
}, [timeout, onTimeout]);
return (
<div className="loading">
<Spinner />
{showSlowMessage && (
<p>This is taking longer than usual...</p>
)}
</div>
);
}
Verification
- Test with Network throttling in DevTools
- Verify timeout errors are caught
- Check UI shows appropriate feedback
- Confirm no hung requests in Network tab