Offline files and durability
Cache request and response pairs
Store a fetched help page in Cache Storage, return a cloned response, and version the cache deliberately.
Cache Storage is not another object database. It stores one thing: a request paired with the response it got. That makes it the right tool for our offline help pages, and the wrong tool for almost anything else.
Fetch, check, store a clone
Four steps: open a named cache, fetch the resource, make sure the response is good, put a copy in the cache.
Here we cache the field guide the app shows when the user has no signal:
const cache = await caches.open('field-notes-help-v1')
const response = await fetch('/field-guide.json')
if (!response.ok) throw new Error(`HTTP ${response.status}`)
await cache.put('/field-guide.json', response.clone())
const guide = await response.json()
Why response.clone()? A response body is a stream, and a stream can be read once. If we handed the original to cache.put(), our own response.json() on the next line would fail with TypeError: body stream already read. The clone gives the cache its own copy, and we keep the original.
After this runs, open the Application panel, expand Cache Storage, and you’ll see field-notes-help-v1 with one entry for /field-guide.json.
fetch() doesn’t reject on 404
This trips up everyone once. fetch() only rejects when the request fails at the network level. A 404 or a 500 is a completed request, so the promise resolves and response.ok is false.
Without the ok check, a temporary server error would replace your good cached guide with an error page. The user goes offline, opens help, and reads “500 Internal Server Error”. Check ok before you put().
Reading it back offline
To serve from the cache, ask it for the request:
const cached = await cache.match('/field-guide.json')
if (cached) {
const guide = await cached.json()
}
match() resolves to undefined when there’s no entry, so handle that. Turn on offline mode in the Network panel and load the help page. It should render from the cache with no network request.
Version the cache name
The name field-notes-help-v1 is your migration boundary. When the guide’s format changes, or you add new assets, open field-notes-help-v2, fill it, and only then delete v1 with caches.delete('field-notes-help-v1').
In that order, there’s never a moment where the user has no cached help. Reusing the same name and overwriting entries one by one leaves a window where half the entries are old and half are new.
No freshness rules for you
An HTTP cache honors Cache-Control and max-age. Cache Storage does not. It keeps whatever you put in it until you remove it. Deciding when a stored response is stale is your job, or your service worker’s. A simple approach is to store a timestamp next to the entry and refetch when it’s older than a day.
Cache the field guide, go offline, and read it with cache.match(). Then point the fetch at a URL that returns 404 and confirm the ok check keeps it out of the cache. The good entry must still be there afterwards.
Lesson completed