Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

If you mean a file users can find in Downloads, use Chrome’s chrome.downloads API. If you mean data your extension needs later, use chrome.storage. They solve different problems—and confusing them is behind many Manifest V3 saving bugs. Here are eight documented failure modes to check, rather than a claim that these are eight personally observed incidents.

First decide: export a file or persist extension state?

chrome.downloads starts and manages downloads in the user’s configured Downloads directory. chrome.storage stores keyed, JSON-serializable extension data for later use. A download completing does not itself save your extension’s state, and storage does not create a user-visible file.

Use the Downloads API when the user needs an exported file. Use extension storage for settings, workflow state, or data needed by future extension events. For sensitive user data, Chrome recommends storage.session; for settings intended to follow a user across synced Chrome browsers, consider storage.sync.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Eight failure modes to check

1. The manifest omits the downloads permission

Declare the permission before calling the Downloads API. Chrome’s documentation states: “You must declare the "downloads" permission in the extension manifest to use this API.” The permission can trigger a user-facing warning, so explain the feature clearly and request only access the extension actually needs.

{
  "manifest_version": 3,
  "permissions": ["downloads"]
}

See the Chrome downloads API reference and Chrome permission guidance.

2. The filename is treated as an absolute filesystem path

The filename option is relative to the user’s Downloads directory, not an arbitrary absolute path. Absolute paths, empty paths, and paths containing .. are rejected. Use a safe relative subdirectory, such as reports/summary.json, if you need files organized into a folder.

chrome.downloads.download({
  url: "https://example.com/summary.json",
  filename: "reports/summary.json"
});

This example illustrates the path format; the URL must identify the actual resource your extension intends to download.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

3. The extension assumes its suggested filename is always accepted

If the filename or MIME type depends on information Chrome determines for the download, use chrome.downloads.onDeterminingFilename. Each extension can register one listener. Every listener invocation must call suggest() exactly once; if the callback is asynchronous, return true so Chrome waits for it.

chrome.downloads.onDeterminingFilename.addListener((item, suggest) => {
  suggest({ filename: "reports/" + item.filename });
});

Choose a filename derived from trusted, validated information and keep it a safe relative path.

4. Another extension also supplies a filename

A download can wait for all filename listeners to call suggest(). If multiple extensions provide overrides, the last installed extension whose listener supplies a suggestion wins. Your extension therefore cannot promise deterministic control over the final name when another extension participates.

5. Existing files collide with the new download

Set conflictAction deliberately rather than leaving the result implicit. The available policies have different consequences:

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Policy Result when a file already exists Use when
uniquify Chrome adds a counter before the file extension. You want to keep the existing file and save another copy.
overwrite The existing file is replaced. Replacement is intended and safe for the user.
prompt The user is asked to choose. The user should decide how to resolve the collision.

For example, a download request can specify both a relative filename and a collision policy:

chrome.downloads.download({
  url: "https://example.com/summary.json",
  filename: "reports/summary.json",
  conflictAction: "uniquify"
});

6. Important state exists only in service-worker globals

Manifest V3 extension service workers can stop after 30 seconds of inactivity. When Chrome shuts one down, its in-memory global variables disappear; a later event can start a fresh worker without that state. Persist values needed by later events in chrome.storage or another appropriate durable mechanism instead of relying on a global variable.

Chrome explains the lifecycle in its extension service worker lifecycle documentation.

7. Web Storage is used for extension state in the wrong context

Service workers cannot access window.localStorage. A content script’s Web Storage calls address the host page’s storage, not the extension’s own storage area. Use the extension Storage API for state that belongs to the extension; it is available in extension contexts including service workers and content scripts.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

See Chrome’s storage API reference and guidance on service worker limitations.

8. The storage lifetime is wrong, or a write is not awaited

Choose a storage area according to how long the data must live and whether it should sync:

Storage area Lifetime and availability Typical fit
storage.local Persists across browser cache or history clearing; is cleared when the extension is removed. Extension state that should remain on this browser.
storage.session Cleared on browser restart, extension reload, disable, or update. Temporary session state, including sensitive user data as Chrome recommends.
storage.sync Intended for settings shared across signed-in, synced Chrome browsers; subject to API quotas. Small user preferences that should follow the user.

chrome.storage operations are asynchronous. Await a write before depending on the value in subsequent logic; starting a download and assuming a storage write has finished is not a persistence guarantee.

await chrome.storage.local.set({ lastExport: "summary.json" });

Storage quotas are API limits and can change. Check the current Chrome storage reference rather than relying on an old quota figure.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Start a download and observe its outcome

chrome.downloads.download() starts a URL download and resolves with a download ID on supported Chrome versions. If both filename and saveAs are specified, Chrome pre-populates the Save As dialog with that filename. The API reference also notes that HTTP and HTTPS requests include cookies set for the hostname, so do not treat this API as a generic file-writing sink for arbitrary in-memory data without validating the URL and data flow.

Use the returned ID to associate the request with later download events. Download states include in_progress, interrupted, and complete. The onChanged event reports property changes, including state and filename; interruptions can reflect file-system, network, server, user, or security errors. Handle failure states instead of treating the initial API call as proof that the file was saved.

const downloadId = await chrome.downloads.download({
  url: "https://example.com/summary.json",
  filename: "reports/summary.json",
  conflictAction: "uniquify"
});

chrome.downloads.onChanged.addListener(delta => {
  if (delta.id === downloadId && delta.state) {
    console.log("Download state:", delta.state.current);
  }
});

Consult the downloads API reference for event properties and documented error details.

Choose the filename strategy that matches what you know

Situation Approach Important detail
The filename is known before starting the download. Pass filename to chrome.downloads.download(). Use a path relative to Downloads and set conflictAction intentionally.
The filename depends on the detected MIME type or tentative filename. Use onDeterminingFilename. Call suggest() exactly once per listener invocation; asynchronous listeners must return true.

Check the target Chrome version

Manifest V3 is generally supported in Chrome 88 and later, but individual APIs and features can require later versions. Verify support for the specific Downloads or Storage behavior you use against Chrome’s API reference index and the relevant API page; do not treat the general MV3 baseline as a guarantee for every feature.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.