Error Handling and Network Failures
Network code fails in more ways than synchronous code. When you call fetch, at least four distinct things can go wrong, and each requires different handling:
- Network failure — no internet, DNS failure, CORS block →
fetchrejects - HTTP error — server responds but with a 4xx or 5xx status →
response.okisfalse - Malformed JSON — response body is not valid JSON →
response.json()rejects - API-level error — server responds with 200 but includes an error in the body → need to check
data.result
A robust fetch function handles all four.
The complete error-handling pattern
Section titled “The complete error-handling pattern”function fetchRates(baseCurrency = 'USD') { return fetch(`https://open.er-api.com/v6/latest/${baseCurrency}`) .then(response => { if (!response.ok) { throw new Error(`Exchange rate API returned ${response.status}`); } return response.json(); }) .then(data => { if (data.result !== 'success') { throw new Error(`API error: ${data['error-type'] ?? 'unknown'}`); } return data.rates; });}The function returns a Promise. Callers add .catch to handle errors in context:
fetchRates() .then(rates => updateCurrencyDisplay(rates)) .catch(error => showErrorBanner(`Could not load exchange rates: ${error.message}`));User-facing error messages
Section titled “User-facing error messages”Log technical error details for debugging, but show users something actionable:
.catch(error => { console.error('fetchRates failed:', error); // for debugging showErrorBanner('Exchange rates unavailable. Showing amounts in USD only.');})Never show raw error messages to users — they are usually meaningless to them and can expose implementation details.
Retrying on failure
Section titled “Retrying on failure”For transient network errors, a simple retry is often appropriate:
function fetchWithRetry(url, retries = 2) { return fetch(url).catch(error => { if (retries > 0) { console.warn(`Retrying (${retries} left)...`); return fetchWithRetry(url, retries - 1); } throw error; });}The .catch returns a new Promise — if there are retries left, it starts a new request. If retries are exhausted, it re-throws and the outer .catch handles it.
Timeout with Promise.race
Section titled “Timeout with Promise.race”fetch has no built-in timeout. You can implement one with Promise.race:
function fetchWithTimeout(url, ms = 5000) { const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Request timed out')), ms) ); return Promise.race([fetch(url), timeout]);}Promise.race settles as soon as the first Promise in the array settles. If the timeout fires first, the fetch is abandoned (though the underlying network request may still be in flight).
Graceful degradation in BudgetBuddy
Section titled “Graceful degradation in BudgetBuddy”When exchange rates are unavailable, BudgetBuddy should still function — just showing amounts in USD rather than crashing:
let rates = { USD: 1 }; // safe default
fetchRates() .then(newRates => { rates = newRates; renderExpenses(expenses, rates); }) .catch(() => { renderExpenses(expenses, rates); // render with USD-only default });The app degrades gracefully. The user sees their expenses even if the exchange rate call fails.
Exercise
Section titled “Exercise”- Write
fetchRates()as shown above, handling all four error cases. - Test the HTTP error path by requesting
https://open.er-api.com/v6/latest/INVALID_CURRENCY— confirmdata.resultis'error'. - Simulate a network failure by going offline in DevTools — confirm the
.catchhandles it. - Implement
fetchWithRetry(url, retries)and test it with a URL that consistently fails. - Add a graceful degradation pattern so BudgetBuddy renders with
{ USD: 1 }rates iffetchRates()fails.
- Four distinct failure modes: network failure, HTTP error, malformed JSON, API-level error — each needs explicit handling.
fetchonly rejects on network failure; checkresponse.okfor HTTP errors.- Show users actionable messages; log technical details separately.
- Retry transient failures with recursion in
.catch. Promise.raceimplements timeouts — the first Promise to settle wins.- Design for graceful degradation — the app should work even when optional features (like currency conversion) fail.