· via dev.to (home feed)
Eight things that broke when a Chrome extension auto-saved JSON to the Downloads folder
A dev.to write-up documents what broke when a Chrome extension auto-saved its data to the Downloads folder via the downloads API under Manifest V3, from missing Blob URLs to cancelled downloads.

A write-up published on dev.to on 25 September 2026 walks through what it took to make a browser extension automatically save a JSON copy of its own data into the user's Downloads folder, and why the seemingly simple downloads.download() call turns into a minefield under Manifest V3. The post carries a disclosure that it was generated by an AI agent (Claude) with no human editing, working from the source code of TabBunker, an MIT-licensed open-source tab manager for Chrome, Edge and Firefox, and from test runs on Chrome for Testing 153 and Firefox 155 in September 2026. It counts eight things that broke along the way.
Why auto-save to Downloads at all
The starting problem is volatility. According to the post, Chrome's documentation states that storage.local data is cleared when an extension is removed, so a single click on Remove, an accidental uninstall or a deleted profile wipes every saved list in a tab manager. The project's answer was a plain JSON file in Downloads/TabBunker/, rewritten shortly after every change, with no server, no account and no file picker. The design keeps one always-current file written with conflictAction: 'overwrite', plus dated snapshots written with conflictAction: 'uniquify' — at most one per hour when something changed, with the newest 30 kept. Writes are scheduled 30 seconds after the first unsaved edit using chrome.alarms, because the Manifest V3 service worker may not still be alive 30 seconds later.
Getting a string to the downloads API
The first breakage is the classic Blob-URL trick. According to the post, URL.createObjectURL does not exist in a Chrome MV3 service worker, while Firefox runs the MV3 background as an event page where it does exist — but in the author's tests Firefox refused a data: URL download with an access-denied error. The fix is feature detection: use createObjectURL when available, otherwise fall back to a data: URL, and revoke Blob URLs once the download settles. The author also measured the data: path with files from 1 MB up to 64 MB; the largest completed in about 1.3 seconds in Chrome for Testing, so real-world payloads sit comfortably within limits.
Overwrite, download history and cleanup
The post found that conflictAction: 'overwrite' behaved the same in both browsers tested, leaving a single file on disk with the newest content. But every write also adds an entry to the browser's download history, so a file rewritten every few minutes would flood the user's download list with junk. The extension therefore erases the previous history entry after each new write completes; erase removes only the history record, not the file on disk. Deleting old snapshots has its own ordering trap: downloads.removeFile(id) must run before downloads.erase({ id }), because after the erase there is no item left to remove. If the user already deleted the file by hand, the item reports exists: false and only the erase is needed. The post adds that if deleting the same file fails three times, snapshotting stops and the failure is surfaced rather than looping.
The id, not the filename
Looking up the extension's own file by name failed in Chrome 153: a filenameRegex query matched against the absolute path, and an exact query using the relative filename the extension itself had passed in returned zero results. The dependable handle, the post concludes, is the numeric id that downloads.download() resolves with, stored for the in-flight write, the current file and every snapshot. A related hazard is that a download outlives the service worker that started it. Three measures made that safe: store the id immediately after the promise resolves; register downloads.onChanged synchronously at the top level of the background script, since Chrome dispatches a wake-up event only to listeners registered during the script's first run; and set a two-minute watchdog alarm that settles finished or interrupted downloads and marks still-running ones as stalled so they can be retried.
Cancellations and lost edits
In Chrome, saveAs: false does not override the browser's ask-where-to-save setting, and the extension's download ended as interrupted with USER_CANCELED — after 277 ms in a headless run. Retrying would pop a save dialog every 30 seconds, so cancellation pauses the backup until the user acts; shutdown and crash simply clear the in-flight marker, and other errors such as disk full or access denied are retried after 5 minutes and then every 30 minutes. Finally, a boolean dirty flag has a race: an edit landing while a backup is in flight can be marked saved when it was not. The replacement is a monotonic revision counter — every edit bumps it, each write records the revision it captured, and completion keeps the maximum.
Why it matters
storage.local is not a durable archive, and extension developers who need persistence without a backend will naturally reach for the downloads API. This post is a practical checklist of the failure modes that approach hits in practice: missing APIs in service workers, browser settings that override extension flags, download-history pollution and races between edits and writes. It is also an interesting data point on machine-assisted technical writing — the author discloses the text was AI-generated from source code and measured test runs, and the specificity of browser versions and timings is precisely what makes the findings credible and reusable.
- #browser-extensions
- #manifest-v3
- #chrome
- #firefox
- #downloads-api