How to Download Files in JavaScript: Complete Guide with Examples
Executive Summary
According to Stack Overflow’s 2023 survey, 63% of developers use JavaScript for file handling, making download functionality an essential skill for modern web development.
This guide covers three primary methods: the classical anchor element approach, the modern Fetch API with Blob objects, and advanced streaming techniques for large files. We’ll walk through production-ready code, common pitfalls you’ll hit if you’re not careful, and when to use each method based on file size, browser support, and performance requirements.
Learn JavaScript on Udemy
Main Data Table: JavaScript File Download Methods
| Method | Browser Support | Best For | File Size Limit | Error Handling |
|---|---|---|---|---|
| Anchor Element (href + download) | All modern browsers | Simple, same-origin files | Typically 500MB | Basic (relies on browser) |
| Fetch API + Blob | Chrome 42+, Firefox 39+, Safari 10.1+ | Cross-origin, JSON, images, PDFs | Available RAM (typically 1-2GB) | Promise-based try/catch |
| Fetch API + Stream (ReadableStream) | Chrome 43+, Firefox 65+, Safari 14.1+ | Large files (video, archives), progress tracking | Unlimited (streamed) | Promise with progress events |
| XMLHttpRequest + ArrayBuffer | All modern browsers | Legacy code, older browser support | Available RAM | Callback-based |
Breakdown by Experience Level & Use Case
The complexity of file downloads scales with your requirements. Beginners typically start with anchor elements and graduate to Fetch API once they need cross-origin downloads or want response validation. Intermediate developers leverage streaming for large files. Advanced scenarios involve service workers for offline support or chunked uploads to handle resumable downloads.
Beginner (Anchor Element): Suitable for small, same-origin files. No JavaScript knowledge required beyond basic syntax.
Intermediate (Fetch + Blob): Handles cross-origin requests, response validation, and moderate file sizes (under 500MB). Requires understanding Promises and async/await.
Advanced (Fetch + ReadableStream): Streaming large files with progress tracking, cancellation support, and memory efficiency. Demands knowledge of streams and event handling.
Expert (Service Workers + Caching): Offline downloads, resumable transfers, and sophisticated error recovery across page reloads.
Method Comparison: Download Approaches
| Aspect | Anchor Element | Fetch + Blob | Fetch + Stream | XMLHttpRequest |
|---|---|---|---|---|
| Code Simplicity | 1 line | 5-10 lines | 15-25 lines | 10-15 lines |
| Cross-Origin Support | Limited (CORS) | Full (CORS) | Full (CORS) | Full (CORS) |
| Progress Tracking | None | None | Yes (via getReader) | Yes (via events) |
| Memory Usage | Minimal | High (entire file in RAM) | Low (streaming) | High (entire file in RAM) |
| Cancellation | No | Via AbortController | Via AbortController | Yes (abort method) |
| Modern Standard | HTML5 | Fetch Standard (2015) | Streams Standard (2016) | Legacy (XMLHttpRequest spec) |
Key Factors for Choosing a Download Method
1. File Size and Memory Constraints
This is the most critical decision point. The Fetch + Blob approach loads the entire file into RAM before creating the download. For a 500MB file, that’s 500MB of memory consumed. If you’re downloading files regularly, this can bloat your browser’s memory footprint. Streaming bypasses this by processing chunks as they arrive, keeping memory usage constant regardless of file size. For files under 50MB on modern systems, Blob is fine. Above 200MB, stream instead.
2. Browser Compatibility Requirements
The anchor element with download attribute works everywhere—Internet Explorer 14 and beyond. Fetch API requires IE 11 with polyfills or just skip IE entirely (most teams have). ReadableStream is the newest, requiring Chrome 43+, Firefox 65+, Safari 14.1+. If you need IE 10 support, you’re stuck with XMLHttpRequest, which is why legacy code still uses it.
3. Cross-Origin and CORS Complexity
Simple anchor downloads trigger browser same-origin policy, requiring server-side proxy for external files. Fetch API respects CORS headers from the remote server—cleaner if you control both endpoints. For third-party APIs, verify they support CORS before committing to Fetch. Many don’t, forcing you back to anchor downloads through your own server.
4. Error Handling and User Feedback
Anchor elements fail silently—the browser either downloads or blocks it. Fetch promises let you catch 404s, timeouts, and network errors before attempting download. Streaming with progress events shows real-time download percentage, essential for files exceeding 10MB where users need feedback. Without it, they’ll assume it’s frozen.
5. Response Validation Before Download
Fetch lets you inspect response headers and partial content before committing to the full download. Check the Content-Type header to verify it’s actually a PDF before downloading “report.pdf”—which might be HTML error page. Anchor elements download whatever arrives, validation happens after.
Historical Trends and Evolution
JavaScript file downloads have followed browser API maturation. Pre-2010, downloads required server-side redirects or plugins. HTML5’s download attribute (2010) enabled client-side anchor downloads, but blocked cross-origin requests. Fetch API (2015) introduced promise-based HTTP with CORS support, but it still loaded files entirely into memory. ReadableStream (2016) finally solved the memory problem for large files, but adoption was slow—Firefox didn’t support it until 2019.
Today’s landscape (April 2026) is dominated by Fetch + Blob for typical workflows and Fetch + Stream for production systems handling gigabyte-scale files. XMLHttpRequest is relegated to legacy maintenance. The anchor element persists for simple cases but is increasingly replaced by programmatic downloads via Fetch.
Expert Tips: Production-Ready Code Patterns
Tip 1: Always Use AbortController for Cancellation
Users change their minds. Abort requests mid-download to save bandwidth. This single-line addition prevents wasted data:
const controller = new AbortController();
fetch(url, { signal: controller.signal });
button.addEventListener('click', () => controller.abort());
Tip 2: Validate Response Status Before Processing
HTTP 200 doesn’t guarantee a real file. Error pages return 200 with HTML content. Check Content-Type:
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const contentType = response.headers.get('content-type');
if (!contentType.includes('application/pdf')) throw new Error('Not a PDF');
Tip 3: Implement Progress Tracking for Files Over 10MB
Silent downloads over 10MB feel broken. Use ReadableStream’s getReader() to track bytes downloaded:
const reader = response.body.getReader();
const total = parseInt(response.headers.get('content-length'), 10);
let downloaded = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
downloaded += value.length;
console.log(`${(downloaded / total * 100).toFixed(1)}%`);
}
Tip 4: Handle Slow Networks with Timeout Logic
Don’t let slow downloads hang indefinitely. Race fetch against a timeout:
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 30000)
);
await Promise.race([fetch(url), timeoutPromise]);
Tip 5: Rename Files Based on Server Headers
The Content-Disposition header often contains the original filename. Use it:
const disposition = response.headers.get('content-disposition');
const filename = disposition?.match(/filename="(.+)"/)?.[1] || 'download';
FAQ Section
Q1: What’s the simplest way to download a file in JavaScript?
A single line using an anchor element: <a href="/file.pdf" download>Download</a>. No JavaScript required. If you must use JavaScript: create the anchor dynamically, set the href and download attributes, trigger click(), then remove it. This works for same-origin files under ~500MB on most browsers and has been the standard approach since HTML5 (2010).
Q2: How do I download files from a different domain?
Use Fetch API with CORS. The remote server must include appropriate CORS headers (Access-Control-Allow-Origin). If they don’t, browsers block direct access, and you’ll need a server-side proxy that fetches the file and streams it to your client. Fetch bypasses the same-origin policy for cross-domain downloads if CORS is enabled, unlike anchor elements which require proxy.
Q3: How can I show download progress to the user?
Use Fetch with ReadableStream and track bytes downloaded against the Content-Length header. Call getReader() on the response body to read chunks, accumulating the total, then calculate percentage. XMLHttpRequest’s progress event is simpler but less flexible. For files under 50MB where progress matters less, skip this complexity.
Q4: What happens if the download fails mid-way?
Wrap fetch in try/catch to handle network errors. The browser won’t automatically retry. Implement retry logic with exponential backoff for critical downloads. For files supporting HTTP 206 Range requests, implement resumable downloads by tracking the last successful byte and requesting only the remaining portion. Most servers support Range, but verify before implementing.
Q5: Can I download files without user interaction?
Yes, but only if the download was user-initiated in the last 5 seconds (varies by browser). Browsers block downloads from pages users didn’t trigger, preventing malware distribution. Click a button, then download—fine. Download on page load—blocked. This is a security feature, not a limitation you can bypass.
Conclusion: Choosing Your Approach
Start with the anchor element for simple files. It works everywhere, requires no JavaScript, and handles 99% of basic download scenarios. When you need cross-origin support, response validation, or real-time progress tracking, graduate to Fetch API with Blobs. For files exceeding 200MB or production systems handling thousands of concurrent downloads, switch to streaming with ReadableStream to preserve memory.
The key insight: complexity should match requirements. An anchor element isn’t “worse” than streaming—it’s appropriate for different scenarios. Production code should implement AbortController support for cancellation and Content-Type validation before processing. Always wrap downloads in error handling, test with files actually larger than your test environment RAM, and monitor memory usage in staging before deploying to production. Your users notice downloads that freeze—they won’t forget.
Learn JavaScript on Udemy
Related tool: Try our free calculator