Handling Offline Mode in Firefox Browser Extensions
When building extensions that depend on network data — like my Weather & Clock Dashboard — offline handling is critical. Users notice immediately when your extension shows stale data or broken UI.
The Problem
Browser extensions run at browser startup. If the user is on a train, airplane, or just has spotty WiFi, your fetch calls will fail. Without proper handling, users see errors or empty states.
Detecting Online Status
The browser gives you two ways to check connectivity:
// Passive check
if (!navigator.onLine) {
showCachedData();
return;
}
// Listen for changes
window.addEventListener('online', () => {
console.log('Back online — refreshing data');
fetchFreshData();
});
window.addEventListener('offline', () => {
console.log('Gone offline — switching to cache');
showOfflineIndicator();
});
Note: navigator.onLine only tells you if you have a network connection — not if that connection actually works. You can be "online" but unable to reach your API.
A Robust Fetch Wrapper
Here's a pattern I use for all API calls in the extension:
async function fetchWithFallback(url, options = {}) {
const CACHE_KEY = `cache_${url}`;
const CACHE_TTL_KEY = `cache_ttl_${url}`;
const TTL_MS = 3600000; // 1 hour
try {
// Attempt network fetch with timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5s timeout
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
// Cache the fresh data
const cacheEntry = {
data,
timestamp: Date.now()
};
localStorage.setItem(CACHE_KEY, JSON.stringify(cacheEntry));
return { data, fromCache: false };
} catch (error) {
console.warn(`Fetch failed for ${url}:`, error.message);
// Try cache
const cached = localStorage.getItem(CACHE_KEY);
if (cached) {
const { data, timestamp } = JSON.parse(cached);
const age = Date.now() - timestamp;
const ageHours = Math.round(age / 3600000);
return {
data,
fromCache: true,
cacheAge: age,
stale: age > TTL_MS
};
}
// No cache available
throw new Error('No data available (offline and no cache)');
}
}
Showing Stale Data Indicators
Users prefer seeing old data over an error. But they deserve to know it's stale:
async function updateWeather() {
const statusEl = document.getElementById('weather-status');
try {
const { data, fromCache, cacheAge } = await fetchWithFallback(WEATHER_API_URL);
renderWeather(data);
if (fromCache) {
const hours = Math.floor(cacheAge / 3600000);
const minutes = Math.floor((cacheAge % 3600000) / 60000);
statusEl.textContent = hours > 0
? `Last updated ${hours}h ago (offline)`
: `Last updated ${minutes}m ago (offline)`;
statusEl.className = 'status-stale';
} else {
statusEl.textContent = `Updated just now`;
statusEl.className = 'status-fresh';
}
} catch (error) {
statusEl.textContent = 'Weather unavailable';
statusEl.className = 'status-error';
}
}
Using the Service Worker Cache API
For heavier caching needs, consider the Cache API (available in extensions with the right permissions):
const CACHE_NAME = 'weather-data-v1';
async function cacheResponse(url, data) {
const cache = await caches.open(CACHE_NAME);
const response = new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' }
});
await cache.put(url, response);
}
async function getCachedResponse(url) {
const cache = await caches.open(CACHE_NAME);
const response = await cache.match(url);
if (response) {
return response.json();
}
return null;
}
Testing Offline Behavior
Chrome/Firefox DevTools let you simulate offline mode:
- Open DevTools → Network tab
- Change "No throttling" dropdown to "Offline"
- Reload your extension's new tab page
For automated testing:
// In Playwright tests
await context.setOffline(true);
await page.reload();
// Should show cached data
const weatherText = await page.textContent('#weather-temp');
expect(weatherText).not.toBe('');
// Should show stale indicator
const statusText = await page.textContent('#weather-status');
expect(statusText).toContain('offline');
Background Refresh Pattern
For new tab extensions, pre-fetch data in the background script so it's ready before the tab opens:
// background.js
chrome.alarms.create('weatherRefresh', { periodInMinutes: 30 });
chrome.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name === 'weatherRefresh') {
try {
const city = await getStoredCity();
const data = await fetchWeatherData(city);
await browser.storage.local.set({
weatherCache: data,
weatherCacheTime: Date.now()
});
} catch (e) {
// Silently fail — cache stays valid
}
}
});
Then in your new tab page, read from storage first:
async function loadWeather() {
const { weatherCache, weatherCacheTime } = await browser.storage.local.get(['weatherCache', 'weatherCacheTime']);
if (weatherCache) {
// Show cache immediately for instant load
renderWeather(weatherCache);
showCacheAge(weatherCacheTime);
}
// Then try to refresh in the background
fetchFreshWeather().then(data => {
renderWeather(data);
clearCacheIndicator();
}).catch(() => {
// Keep showing cached data
});
}
This gives users instant perceived performance even on slow connections.
The Weather & Clock Dashboard Approach
In my extension, I use localStorage for persistence with a 1-hour TTL. The offline indicator shows as a small cloud icon next to the temperature when the data is stale.
Try it out: Weather & Clock Dashboard on AMO
What's your approach to offline handling in extensions? Let me know in the comments!
Part of a series on building Firefox browser extensions.
Top comments (0)