When an API call fails, applications that render nothing leave users with no way to understand what happened or what they can still do. ISO 25010 reliability.fault-tolerance expects partial operation: a profile page should still show the edit form even if the user data fetch fails; a dashboard should still show navigation even if a widget's data source is unavailable. Applications that hard-fail to a blank screen or infinite spinner on any API error eliminate user agency entirely — an especially high-impact defect in payment and checkout flows.
Medium because full-page blank screens on API failure eliminate user agency and produce abandonment at the exact moments — payment, onboarding — where recovery matters most.
Separate required data (block render) from optional data (degrade gracefully). For optional API calls, always return a typed fallback on failure so the page can render partially.
// lib/api.ts — safe fetch helper
export async function safeFetch<T>(
url: string,
fallback: T
): Promise<T> {
try {
const res = await fetch(url)
if (!res.ok) return fallback
return res.json() as Promise<T>
} catch {
return fallback
}
}
// Usage:
const widgets = await safeFetch('/api/dashboard/widgets', [])
// Page renders even if widgets is []
For required data that must succeed (e.g., auth session), let the error propagate to the nearest error boundary rather than swallowing it.
ID: error-resilience.network-api-resilience.graceful-api-failure
Severity: medium
What to look for: Count all API response handling locations. Enumerate which handle non-200 status codes gracefully vs. which crash or show raw errors. Look at components that fetch data. Check whether they render partial content when an API fails (e.g., show the form without pre-filled data, show the page without the widget, etc.) or if they show an infinite spinner/blank screen.
Pass criteria: When an API call fails, the app renders partial UI (form without pre-filled data, page without optional widgets) and informs the user that data is stale or incomplete. At least 90% of API call sites must handle non-200 responses with user-friendly fallback behavior.
Fail criteria: API failure results in blank screen, infinite spinner, or empty state with no context or ability to proceed.
Skip (N/A) when: The application is a static site or has no data fetching components.
Cross-reference: For data fetching loading/error states, see data-fetching-states.
Detail on fail: "When user API fails, entire profile page is blank with no spinner or error message" or "Dashboard shows infinite spinner when optional widgets fail to load; user cannot access page at all"
Remediation: Render partial UI even when some APIs fail:
// lib/api.ts — graceful API error handling
const res = await fetch(url)
if (!res.ok) { return { error: true, message: 'Service temporarily unavailable', data: null } }
export function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState(null)
const [error, setError] = useState(null)
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(setUser)
.catch(setError)
}, [userId])
// Render something, even on error
return (
<div>
<h1>User Profile</h1>
{user ? (
<>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
</>
) : (
<p>Unable to load user details. Some information may not be available.</p>
)}
{error && <p>Error: {error.message}</p>}
{/* Show edit form even without pre-filled data */}
<EditProfileForm initialUser={user} />
</div>
)
}