Offline Fallback — Graceful Degradation When Network Fails
In this tutorial, you will learn about Offline Fallback. We cover key concepts, practical examples, and best practices to help you master this topic.
Offline fallback patterns ensure your PWA displays meaningful content instead of errors when the network is unavailable, using cached pages and placeholder responses to maintain user trust.
What You'll Learn
By the end of this tutorial, you will understand how to implement offline fallbacks for navigation, images, API calls, and third-party content, creating a robust experience regardless of network state.
Why It Matters
Users do not care why the network failed. They care that your app is broken. A well-designed offline fallback keeps users engaged, shows that your app is thoughtfully built, and prevents the frustration of broken pages or infinite loading spinners.
Real-World Use
A recipe PWA shows a "You are offline — here are your recently viewed recipes" page when the network drops. Each recipe card is cached from previous visits. Instead of a generic error, the user sees useful content and can continue cooking.
Fallback Hierarchy
Offline Fallback Decision Tree
Network request fails
↓
┌──────────────────────────────────────┐
│ What type of request failed? │
└──────────────────────────────────────┘
↓ ↓ ↓
Navigation Image/Asset API/Data
↓ ↓ ↓
Cached HTML Placeholder Cached JSON
page image response
↓ ↓ ↓
Generic Fallback "Data may
offline icon SVG be stale"
page indicator
Think of offline fallbacks like a restaurant's contingency plan. If the kitchen runs out of beef, they offer chicken instead (alternative). If the whole kitchen is down, they offer free drinks and apologize (generic fallback). They never leave you standing at the counter with no response.
Navigation Fallback
When a user navigates to a page that is not cached, serve a generic offline page:
self.addEventListener('fetch', event => {
if (event.request.method !== 'GET') return;
// Handle navigation requests
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request)
.catch(() => {
console.log('Navigation failed, serving offline page');
return caches.match('/offline.html');
})
);
return;
}
// Handle other requests
event.respondWith(
fetch(event.request)
.catch(() => {
return caches.match(event.request);
})
);
});
Create the offline page:
<!-- offline.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>You Are Offline</title>
<style>
body {
font-family: system-ui, sans-serif;
text-align: center;
padding: 2rem;
background: #f8f9fa;
}
.offline-icon { font-size: 4rem; margin-bottom: 1rem; }
h1 { color: #333; }
p { color: #666; max-width: 400px; margin: 1rem auto; }
.retry-btn {
background: #3367D6;
color: white;
border: none;
padding: 0.75rem 2rem;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
</style>
</head>
<body>
<div class="offline-icon">⚠</div>
<h1>No Internet Connection</h1>
<p>It looks like you are offline. Check your connection and try again.</p>
<button class="retry-btn" onclick="window.location.reload()">
Try Again
</button>
</body>
</html>
Pre-cache the offline page during install:
self.addEventListener('install', event => {
event.waitUntil(
caches.open('offline-fallback-v1').then(cache => {
return cache.addAll([
'/offline.html',
'/styles/offline.css'
]);
}).then(() => self.skipWaiting())
);
});
Image Fallback
When images fail to load offline, serve a placeholder:
self.addEventListener('fetch', event => {
if (event.request.destination === 'image') {
event.respondWith(
fetch(event.request)
.then(response => {
// Cache successful image responses
if (response.ok) {
const clone = response.clone();
caches.open('image-cache').then(cache => {
cache.put(event.request, clone);
});
}
return response;
})
.catch(() => {
return caches.match(event.request).then(cachedImage => {
if (cachedImage) {
console.log('Serving cached image:', event.request.url);
return cachedImage;
}
// Return inline SVG placeholder
console.log('Serving image placeholder');
return new Response(
`<svg xmlns="http://www.w3.org/2000/svg"
width="300" height="200"
viewBox="0 0 300 200">
<rect fill="#e0e0e0" width="300" height="200"/>
<text fill="#999" font-size="14"
x="50%" y="50%" text-anchor="middle"
dominant-baseline="middle">
Image Offline
</text>
</svg>`,
{ headers: { 'Content-Type': 'image/svg+xml' } }
);
});
})
);
}
});
API Fallback with Stale Data Indicator
For API calls, serve cached data and indicate staleness:
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
if (url.pathname.startsWith('/api/')) {
event.respondWith(
fetch(event.request)
.then(response => {
if (response.ok) {
const clone = response.clone();
caches.open('api-cache').then(cache => {
cache.put(event.request, clone);
});
}
return response;
})
.catch(() => {
return caches.match(event.request).then(cached => {
if (cached) {
// Add a header to indicate stale data
const headers = new Headers(cached.headers);
headers.append('X-Data-Stale', 'true');
return new Response(cached.body, {
status: cached.status,
statusText: cached.statusText,
headers: headers
});
}
return new Response(
JSON.stringify({ error: 'offline', message: 'Data unavailable offline' }),
{
status: 503,
headers: { 'Content-Type': 'application/json' }
}
);
});
})
);
}
});
In your page JavaScript:
// Detect stale data in API responses
fetch('/api/posts')
.then(response => {
if (response.headers.get('X-Data-Stale') === 'true') {
console.log('Showing cached data — may not be current');
document.getElementById('stale-indicator').style.display = 'block';
}
return response.json();
});
Third-Party Fallback
For third-party resources like analytics or CDN scripts, skip them gracefully:
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// Third-party analytics — never block page load
if (url.hostname === 'analytics.example.com' ||
url.hostname === 'cdn.thirdparty.com') {
event.respondWith(
fetch(event.request)
.catch(() => {
// Return empty response, do not show error
return new Response('', { status: 200 });
})
);
return;
}
});
Offline Analytics
Even offline, you can track user actions and send them when connectivity returns:
// Service worker
self.addEventListener('fetch', event => {
if (event.request.url.includes('/analytics/')) {
// Queue analytics events in IndexedDB instead of failing
event.respondWith(
fetch(event.request).catch(() => {
// Store the event for later
const eventData = {
url: event.request.url,
method: event.request.method,
timestamp: Date.now()
};
// Store in IndexedDB (pseudo-code)
storeAnalyticsEvent(eventData);
return new Response(JSON.stringify({ queued: true }), {
headers: { 'Content-Type': 'application/json' }
});
})
);
}
});
Common Mistakes
- Generic error messages. "Something went wrong" is unhelpful. Show a friendly, actionable message and suggest retrying or checking the connection.
- Not pre-Caching the offline page. If the offline page itself is not cached, the user sees the browser's default error page, not your branded fallback.
- Caching error responses. If fetch() returns a 500 error and you cache it, offline fallback serves the error instead of useful content.
- Showing stale data without indication. Users lose trust if they do not know data is cached. Always indicate when data may be stale.
- Blocking on third-party resources. If analytics goes down, your app should not break. Always provide fallbacks for non-critical third-party content.
Practice Questions
- What three types of fallback should you implement for a robust PWA?
- Why should you pre-cache the offline fallback page during install?
- How do you indicate to the user that displayed data is from cache and may be stale?
- What response should you return for failed third-party resource requests?
- How would you queue analytics events for delivery when connectivity returns?
Challenge: Implement a complete offline fallback system: a branded offline page for navigation, SVG placeholders for images, cached JSON with stale indicator for API calls, and graceful skip for third-party analytics. Test by going offline in DevTools.
FAQ
Mini Project
Build a complete offline fallback system for a simple blog PWA. Create: 1) a branded offline.html page with retry button, 2) SVG image placeholders, 3) stale data indicator for article lists, and 4) graceful failure for a third-party weather widget. Pre-cache the offline page. Test by going offline in DevTools.
What's Next
Manual service workers are powerful but complex. Next, learn Workbox, Google's library that simplifies service worker generation with pre-built strategies and caching patterns.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro