Understand API responses, manage errors gracefully, keep UI state consistent, and deliver great UX.
Catch runtime exceptions early to prevent crashes and log useful information.
try {
riskyOperation();
} catch (err) {
console.error(err.name); // e.g. "TypeError"
console.error(err.message); // e.g. "Cannot read..."
console.error(err.stack); // Full Call Stack
}
Every HTTP response returns a status code. Interpret them to execute correct UI logic.
Everything worked as expected.
Issues with the request sent by client.
Issues on the server side.
APIs return consistent JSON objects for error payloads. Read res.ok and parse error details.
{
"status": 422,
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": [
{ "field": "email", "issue": "Invalid format" }
]
},
"traceId": "ab12cd34ef56"
}
fetch('/api/users')
.then(res => {
if (!res.ok) {
return res.json().then(err => { throw err; });
}
return res.json();
})
.catch(err => console.error('API Error:', err));
fetch() does NOT throw on HTTP 404 or 500 errors! Always check res.ok manually.
Classifying errors allows your application to execute targeted recovery actions instead of generic alerts.
User input issues or bad form data.
Token expired or missing permissions.
Server crash, DB down, or timeout.
finally Block
Always clear loading indicators and release resources regardless of whether the API call succeeds or fails.
Execute API call. Clean, readable code without nested callback pyramids.
Intercept network failures or non-2xx statuses safely without crashing UI.
Guaranteed execution. Reset loading spinners, re-enable buttons, keep state clean.
async function loadReport() {
setLoading(true); // 1. Show Spinner
try {
const res = await fetch('/api/report');
if (!res.ok) throw new Error('API Failed');
const data = await res.json();
renderReport(data);
} catch (err) {
showError(err.message);
} finally {
setLoading(false); // 2. Always Hide Spinner!
}
}
Design for every state explicitly to ensure users are always informed, confident, and in control.
Ready for user trigger. Show default view.
Show spinner / skeleton. Disable actions.
Render returned data & confirm completion.
No records found. Offer filter reset or create.
Clear error message & offer Retry button.
const [state, setState] = useState({ status: 'idle'|'loading'|'success'|'empty'|'error', data: [], error: null });
Combining state management, status verification, data structure validation, and error normalization.
async function loadProducts() {
state.loading = true; state.error = null; updateUI();
try {
const res = await fetch('/api/products');
if (!res.ok) throw new ServerError(`HTTP ${res.status}: Failed to load`);
const data = await res.json();
if (!Array.isArray(data)) throw new DataError('Invalid payload format');
state.data = data;
} catch (err) {
state.error = normalizeError(err);
} finally {
state.loading = false; updateUI();
}
}
Provide pathways to recover from network drops without forcing a full page reload.
If a request fails due to temporary network glitch, automatically retry up to N times with increasing delays (e.g., 500ms, 1000ms, 2000ms).
Users don't care about your internal stack trace. Help them understand what happened and how to fix it.
Follow this master checklist to ensure high application reliability and data integrity.
res.ok and handle success & failure.finally block.!res.ok and assuming HTTP 200.