Simulating API Errors & Network Delays: Chaos Engineering for Frontend Applications
Simulating API Errors & Network Delays: Chaos Engineering for Frontend Applications
Most frontend testing is trapped in the Happy Path. Engineers write components against fast local environments with 0ms network latency and 100% API success rates.
Then the application ships to real users on subway trains with intermittent 3G connections, corporate firewalls that drop WebSocket packets, and overloaded backend servers that throw sudden 503 Service Unavailable errors. The result? Unhandled runtime exceptions, white screens of death, and angry customer tickets.
Frontend Chaos Engineering is the practice of intentionally injecting network latency, packet failures, and HTTP error codes into your client during development to verify that the user interface degrades gracefully.
In this guide, you will master the full spectrum of HTTP status codes your frontend must handle, build a production-grade exponential backoff retry utility, and simulate chaos using Gen-Endpoint's Status API and Delay API.
The Taxonomy of HTTP Errors Your UI Must Handle
Your frontend should not treat all errors as generic "Something went wrong" messages. Different status code categories require fundamentally different UX treatments:
| HTTP Status | Meaning | Typical Cause | Required Frontend UX Action | | :--- | :--- | :--- | :--- | | 400 Bad Request | Invalid syntax / malformed JSON | Client bug | Display general alert; log to Sentry | | 401 Unauthorized | Missing or expired token | Auth session expired | Clear auth cookies; redirect to login with return path | | 403 Forbidden | Authenticated, but lacks rights | Role permission restriction | Render an inline "Permission Denied" view | | 404 Not Found | Resource does not exist | Broken link or deleted item | Render empty state with navigation links back to list | | 422 Unprocessable | Semantic validation failure | Field failed regex or range | Highlight specific invalid input fields in red | | 429 Too Many Req | Rate limit exceeded | User or script spamming API | Show "Slow down" notice with countdown timer | | 500 Server Error | Unhandled backend exception | Database failure or unhandled crash | Show retry button; keep user form inputs intact | | 503 Unavailable | Server temporarily down | Deployment or overload | Offer retry with exponential backoff |
Simulating Intentional HTTP Status Codes
With Gen-Endpoint's Status API, you can trigger any standard HTTP status code on demand by appending the code to the URL:
# Trigger an immediate 404 Not Found
curl -I https://gen-endpoint.com/api/status/404
# Trigger a 429 Rate Limit Exceeded
curl -I https://gen-endpoint.com/api/status/429
# Trigger a 500 Internal Server Error
curl -I https://gen-endpoint.com/api/status/500
This enables automated integration tests (such as Playwright or Cypress) to assert that your error toast notification appears whenever a 500 occurs.
Building an Exponential Backoff & Jitter Utility
When a network request fails due to transient network drops or server restarts (such as HTTP 502, 503, or 504), hammering the server with immediate retries makes the outage worse (a "retry storm").
The industry-standard pattern is Exponential Backoff with Full Jitter: $$\text{delay} = \text{random}(0, \min(\text{maxDelay}, \text{baseDelay} \times 2^{\text{attempt}}))$$
Here is a typed TypeScript fetch wrapper implementing this algorithm:
// utils/fetchWithRetry.ts
interface RetryOptions {
retries?: number;
baseDelayMs?: number;
maxDelayMs?: number;
retryableStatuses?: number[];
}
export async function fetchWithRetry(
url: string,
init?: RequestInit,
options: RetryOptions = {}
): Promise<Response> {
const {
retries = 3,
baseDelayMs = 300,
maxDelayMs = 3000,
retryableStatuses = [408, 429, 500, 502, 503, 504],
} = options;
let attempt = 0;
while (attempt <= retries) {
try {
const response = await fetch(url, init);
// If response is successful or error is non-retryable (e.g. 401 or 404), return immediately
if (response.ok || !retryableStatuses.includes(response.status)) {
return response;
}
// Check if server specified a Retry-After header
const retryAfterHeader = response.headers.get('Retry-After');
let waitMs = 0;
if (retryAfterHeader) {
const seconds = parseInt(retryAfterHeader, 10);
waitMs = !isNaN(seconds) ? seconds * 1000 : baseDelayMs;
} else {
// Calculate exponential backoff with full jitter
const exponentialDelay = Math.min(maxDelayMs, baseDelayMs * Math.pow(2, attempt));
waitMs = Math.random() * exponentialDelay;
}
attempt++;
if (attempt > retries) {
return response; // Return final failed response
}
console.warn(`[Retry] Attempt ${attempt} failed with status ${response.status}. Retrying in ${Math.round(waitMs)}ms...`);
await new Promise((resolve) => setTimeout(resolve, waitMs));
} catch (networkError) {
// Network drop / offline failure
attempt++;
if (attempt > retries) {
throw networkError;
}
const exponentialDelay = Math.min(maxDelayMs, baseDelayMs * Math.pow(2, attempt));
const waitMs = Math.random() * exponentialDelay;
console.warn(`[Retry] Network error on attempt ${attempt}. Retrying in ${Math.round(waitMs)}ms...`);
await new Promise((resolve) => setTimeout(resolve, waitMs));
}
}
throw new Error('Maximum retry attempts exceeded.');
}
Simulating Latency & Timeouts with the Delay API
Spinners that flash for 30ms look jarring; long requests that freeze the UI without feedback frustrate users. Using Gen-Endpoint's Delay API, you can delay any HTTP response by an exact number of milliseconds:
# Simulate a realistic 3G mobile latency of 1200ms
curl https://gen-endpoint.com/api/delay/1200
# Simulate a heavy 3000ms database report query
curl https://gen-endpoint.com/api/delay/3000
Testing Client-Side Abort Timeouts
Never allow a fetch request to hang indefinitely. Use AbortSignal.timeout to cancel requests that take longer than your SLA:
async function fetchWithTimeout(url: string, timeoutMs: number = 2000) {
try {
const response = await fetch(url, {
signal: AbortSignal.timeout(timeoutMs),
});
return await response.json();
} catch (error: any) {
if (error.name === 'TimeoutError') {
console.error(`Request timed out after ${timeoutMs}ms!`);
// Render timeout alert to user with retry button
}
throw error;
}
}
// Test against Gen-Endpoint: This will abort because the server takes 3000ms while timeout is 1500ms
fetchWithTimeout('https://gen-endpoint.com/api/delay/3000', 1500);
The Frontend Resilience Checklist
Before approving any pull request that involves API communication, verify:
- Skeleton States: Does the UI show an appropriate skeleton loader during the first 500ms–2000ms?
- Double-Click Protection: Are submission buttons disabled during inflight requests to prevent duplicate transactions?
- Preserved Inputs: If a form submission encounters a
500error, does the form keep all typed values intact so the user doesn't have to retype everything? - Offline Handling: What happens if the user's connection drops while interacting with the page?
- Clear Error Copy: Does the error message clearly explain what happened and what action the user should take?
Summary & Live Practice
Engineering resilient web applications requires actively testing worst-case network scenarios. By pairing client retry logic with Gen-Endpoint's Status API and Delay API, you can guarantee your UI survives real-world chaos.
- Test simulated HTTP codes live on the Status API Page.
- Test latency thresholds on the Delay API Page.
Ready to Test These Endpoints?
Access 23+ free mock REST APIs, test file uploads, and generate Next.js API handlers with zero signup.