How to Download a File With JavaScript
Learn the right JavaScript download pattern for URLs, generated Blobs, and fetched files, with cleanup, CORS, errors, and complete examples.
Use an anchor with the download attribute when a file already has a usable URL. For content generated in JavaScript, create a Blob, turn it into a temporary object URL with URL.createObjectURL(), and click a temporary download link. Revoke the object URL after the browser has started the download.
The correct approach depends on where the bytes are:
| Situation | Recommended approach |
|---|---|
| File already exists at a same-origin URL | Use an <a download> link |
| JavaScript creates the content | Create a Blob, then download its object URL |
| You must inspect or transform a remote response | fetch() it, convert it to a Blob, then use an object URL |
| Browser extension code | Use the WebExtensions downloads API with its extension permissions |
1. Download a file that already has a URL
For a file your page can link to, add download to an anchor. A value such as report.pdf suggests the filename.
<a href="/files/report.pdf" download="report.pdf">
Download the report
</a>
You can create the link from JavaScript when the filename or URL is dynamic:
function downloadUrl(url, filename) {
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.textContent = `Download ${filename}`;
document.body.append(link);
}
downloadUrl("/files/report.pdf", "report.pdf");
According to MDN’s documentation for the anchor element, the attribute works for same-origin URLs and for blob: and data: URLs. It is not a general way to force a download from an arbitrary cross-origin URL. Browser settings and server response headers can also affect the final handling.
2. Download text, JSON, or other data generated in JavaScript
Put the generated bytes in a Blob, create an object URL, and activate an anchor in response to the user’s action.
function downloadText(text, filename = "download.txt") {
const blob = new Blob([text], {
type: "text/plain;charset=utf-8"
});
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.append(link);
link.click();
link.remove();
// Let the browser begin using the URL before cleanup.
setTimeout(() => URL.revokeObjectURL(url), 0);
}
document.querySelector("#download-text").addEventListener("click", () => {
downloadText("name,score\nAda,100\n", "scores.csv");
});
<button id="download-text" type="button">Download CSV</button>
URL.createObjectURL(blob) returns a temporary reference to the Blob’s data. It is not a permanent file URL. Release it with URL.revokeObjectURL() when the browser no longer needs it; otherwise a long-running page can retain unnecessary object-URL resources. See MDN’s createObjectURL documentation.
Generate JSON
function downloadJson(value, filename = "data.json") {
const json = JSON.stringify(value, null, 2);
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.append(link);
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(url), 0);
}
downloadJson({ user: "Ada", active: true }, "user.json");
Generate a browser File
A File is a specialized kind of Blob. APIs that accept a Blob can also accept a File.
function downloadFile(text, filename) {
const file = new File([text], filename, {
type: "text/plain;charset=utf-8",
lastModified: Date.now()
});
const url = URL.createObjectURL(file);
const link = document.createElement("a");
link.href = url;
link.download = file.name;
document.body.append(link);
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(url), 0);
}
3. Fetch a remote file, then download it
Use this pattern when JavaScript must read, inspect, or transform the response before offering it to the user.
async function downloadRemoteFile(url, filename) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Download failed: ${response.status}`);
}
const blob = await response.blob();
const objectUrl = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = objectUrl;
link.download = filename;
document.body.append(link);
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(objectUrl), 0);
}
document.querySelector("#download").addEventListener("click", async () => {
try {
await downloadRemoteFile("/files/archive.zip", "archive.zip");
} catch (error) {
console.error(error);
alert("The download could not be started.");
}
});
The remote server must allow the page to read the response. A browser page cannot bypass the server’s cross-origin policy. If the server does not permit the request, use a same-origin backend or a direct link that lets the server handle the download. For a simple server-hosted file, a direct link can also avoid buffering the entire response in JavaScript.
Read a filename from response headers
If your server sends a filename in Content-Disposition, you can parse it on your server or expose it to browser JavaScript through the response’s CORS headers. A safer client-side fallback is to use a known filename when the header is unavailable.
function filenameFromUrl(url, fallback = "download") {
try {
const lastPart = new URL(url, location.href).pathname.split("/").pop();
return lastPart ? decodeURIComponent(lastPart) : fallback;
} catch {
return fallback;
}
}
4. Download binary data correctly
Use the response method that matches the content. response.blob() preserves binary data for images, archives, PDFs, and similar files. Do not convert arbitrary binary data to a string first.
async function downloadImage(url, filename = "image.png") {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const blob = await response.blob();
const objectUrl = URL.createObjectURL(blob);
const link = Object.assign(document.createElement("a"), {
href: objectUrl,
download: filename
});
document.body.append(link);
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(objectUrl), 0);
}
5. Common options and browser behavior
download="name.ext": suggests the saved filename. It does not guarantee that every browser or user setting will use it.BlobMIME type: settypeto the actual format, such asapplication/pdfortext/csv;charset=utf-8.- Object URL cleanup: revoke each URL after the download has started, and revoke longer-lived URLs when the referenced resource is no longer needed.
- Server headers: a server can influence inline versus attachment handling and the filename. The anchor attribute is a request, not an absolute guarantee.
- User activation: start downloads from a click or other user action. Browsers may restrict unsolicited downloads.
- Multiple files: avoid triggering many downloads at once; browsers may block them. Package files into one archive on the server when that is practical.
6. Performance, memory, and reliability
- Direct URL: prefer an ordinary link when no transformation is required. The browser and server can handle the transfer without first copying the complete file into a JavaScript Blob.
- Fetched Blob: the fetch-then-download pattern holds the response in memory while creating the Blob. Very large files can increase memory pressure; use a direct server response when you do not need client-side processing.
- Object URLs: create them only when needed and revoke them after use. Do not revoke a URL while an image, link, or download still needs it.
- Retries: check
response.ok, show a useful error, and offer a retry for transient failures. Do not retry a response that failed because the server denied cross-origin access. - Integrity: use HTTPS and validate the expected content type or size when downloads are security-sensitive.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The link opens the file instead of saving it | Browser settings or server headers override the requested behavior | Use a server response configured for attachment, or let the user save from the opened resource |
fetch() fails with a CORS error |
The remote server did not allow the page to read the response | Configure the server, proxy through your own backend, or use a direct link |
| The downloaded file is corrupted | Binary bytes were converted to text | Use response.blob() or another binary-safe API |
| The suggested filename is ignored | Browser or server behavior controls the final name | Check Content-Disposition, use a same-origin URL, and treat download as a suggestion |
| Memory grows after repeated downloads | Object URLs were never revoked | Call URL.revokeObjectURL() after each temporary URL is no longer needed |
| Nothing happens after programmatic click | Download was not started from a user gesture, or the browser blocked it | Call the function directly inside a click handler and show an explicit fallback link |
A remote URL works in an anchor but not with fetch() |
Navigation and script-readable cross-origin requests have different rules | Keep the direct link or add server-side CORS support |
8. Complete browser example
<button id="save" type="button">Download report</button>
<script>
function downloadText(text, filename) {
const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.append(link);
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(url), 0);
}
document.querySelector("#save").addEventListener("click", () => {
const report = [
"Monthly report",
"==============",
"Orders: 128",
"Refunds: 3"
].join("\\n");
downloadText(report, "monthly-report.txt");
});
</script>
9. Equivalent command-line and server-side downloads
These examples are useful when the browser should not handle the transfer itself.
cURL
curl -L "https://example.com/files/report.pdf" -o report.pdf
Python
import requests
response = requests.get("https://example.com/files/report.pdf", timeout=90)
response.raise_for_status()
with open("report.pdf", "wb") as file:
file.write(response.content)
Node.js
const response = await fetch("https://example.com/files/report.pdf");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const bytes = new Uint8Array(await response.arrayBuffer());
await import("node:fs/promises").then(fs => fs.writeFile("report.pdf", bytes));
10. Browser pages versus extensions
普通 web pages use anchors, Blobs, object URLs, and fetch(). Browser extensions have a separate, privileged WebExtensions downloads API with extension-specific permissions and behavior. Do not copy extension examples into ordinary page JavaScript without adapting the architecture.
11. Security and accessibility checklist
- Use a real
<button>for generated downloads and a descriptive link for existing files. - Tell users what format and filename they will receive.
- Do not place untrusted strings directly into HTML; assign them with DOM properties.
- Validate URLs and filenames when they come from user input.
- Use HTTPS for sensitive files and protect authenticated downloads with normal server authorization.
- Provide visible error messages and a retry or fallback link.
Or skip the browser setup
If your goal is to download screenshots of web pages rather than build a browser download flow, ScreenshotNeo returns a PNG, JPEG, WebP, or PDF from one GET request. It accepts cookie banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result.
Use the API directly (see the ScreenshotNeo API docs):
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo also provides an MCP server for Claude, Cursor, and other MCP clients, with take_screenshot, get_page_info, and capture_pdf tools. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
FAQ
Can JavaScript force any URL to download?
No. The download attribute is supported for same-origin, blob:, and data: URLs. Cross-origin behavior depends on the server and browser.
Why do I need URL.revokeObjectURL()?
Object URLs hold a reference to Blob data. Revoking them releases that reference when it is no longer needed.
Should I use a Blob for every download?
No. Use a direct link when the file is already hosted and does not need client-side inspection or transformation.
Can I download a file without a user click?
Browsers may block unsolicited or repeated downloads. Start the operation from a user action and provide a visible fallback.
What is the difference between a Blob and a File?
A File is a Blob with a name and related file metadata. Both can be used with object URLs.


