Networks are unpredictable and APIs fail. Defensive JavaScript ensures your UI stays fast, consistent, and resilient under real-world conditions.
AbortController
Set strict time limits on API calls to stop background memory leaks and free network sockets.
Cancels in-flight network requests, stops unnecessary server processing, and prevents stale state updates.
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 8000);
try {
const res = await fetch('/api/users', { signal: controller.signal });
clearTimeout(timeoutId);
const data = await res.json();
} catch (err) {
if (err.name === 'AbortError') {
showUserMessage('Request timed out. Please try again.');
}
}
fetch(url, { signal: AbortSignal.timeout(8000) })
Retry failed requests with exponentially increasing delays to avoid crushing recovering servers.
delay = base * 2^attemptdelay * (0.5 + Math.random())
GET, HEAD, OPTIONS, PUT, DELETE (if designed safe).
POST calls (may create duplicate orders/charges).
When users click rapidly, older slow requests can arrive late and overwrite fresh UI state.
Request A (slow) sent first. Request B (fast) sent second. Request A arrives LAST and overwrites B's data!
Track an incremental lastRequestId. Only update UI state if the response matches the latest ID.
let lastRequestId = 0;
async function loadUsers() {
const requestId = ++lastRequestId; // Increment ID
const res = await fetch('/api/users');
const data = await res.json();
if (requestId === lastRequestId) {
setUsers(data); // Only apply if still fresh!
}
}
Stop double submissions at both UI controls and network header layers.
Validate payloads early on the client to save network roundtrips, and enforce strict server schemas.
Never assume API payloads match expected schemas. Guard property access and provide fallbacks.
Prevents "Cannot read property of undefined" crashes.
Provides safe defaults when fields are null or undefined.
Verify arrays with Array.isArray() before calling .map().
const user = payload?.user ?? {};
const cleanUser = {
name: user.name?.trim() || 'Anonymous',
email: user.email ?? 'No email provided',
orders: Array.isArray(payload?.orders) ? payload.orders : [],
avatar: user.avatar?.url ?? '/default-avatar.png'
};
Translate technical exception objects into simple, actionable user UI choices.
Model every asynchronous request state explicitly to prevent UI flicker and state inconsistencies.
Initial state before request starts. UI ready.
Request in progress. Show spinner or skeleton screens.
Data loaded cleanly. Render full datasets.
Failed call. Preserve last valid data & show retry button.
{ status: 'idle'|'loading'|'success'|'error', data: [], error: null }
Combining AbortController, Timeout, Exponential Backoff, HTTP checks, and Safe Defaults.
async function fetchResilient(url, { maxRetries = 3, timeoutMs = 5000 } = {}) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
if (attempt === maxRetries) throw err;
const delay = Math.pow(2, attempt) * 500;
await new Promise(r => setTimeout(r, delay));
}
}
}
Master these rules to transform fragile scripts into production-ready web applications.