Stop Corrupting Base64 Data URL Downloads Client-Side
So your API just handed you a base64 string and said "good luck" — now what?
You're building a frontend feature. Maybe it's a PDF invoice generator, maybe it's an image export tool, maybe it's a document preview system. Your backend or third-party API returns a base64-encoded file, and your job is to turn that wall of characters into a clean, downloadable file — entirely in the browser, no server round-trip allowed.
I've watched dozens of developers hit the same wall here. The base64 string arrives. You slap it into an <a> tag. You click. Nothing happens. Or worse: a file downloads, but it's corrupted. Or it works perfectly in Chrome and completely fails in Safari. Sound familiar?
Let's walk through the real pitfalls of converting base64 data URLs back to downloadable files client-side — and more importantly, how to fix each one.
How do I convert a base64 data URL to a downloadable file in JavaScript?
Here's the clean, reliable approach that works across modern browsers:
function downloadBase64File(base64Data, filename) {
const link = document.createElement('a');
link.href = base64Data;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
If your base64 string already includes the data: prefix (like data:application/pdf;base64,JVBERi0xLj...), you're good to go. The browser handles the decoding internally when you assign it to href and trigger the download.
The critical detail most guides skip: you must append the link to the DOM before clicking it. Firefox quietly ignores click events on detached elements. I've seen developers spend an hour debugging "it works in Chrome but not Firefox" when the fix was one line: document.body.appendChild(link).
Why does my downloaded base64 file open as corrupted or empty?
This is the number one issue I see, and it almost always comes down to one of three mistakes:
Mistake 1: Missing or wrong MIME type in the data URL
Your base64 string needs a proper data URL prefix. If your API returns just the raw base64 payload without the data:[mimeType];base64, header, the browser has no idea what to do with it. You need to construct the full data URL yourself:
const mimeType = 'application/pdf';
const dataUrl = `data:${mimeType};base64,${rawBase64String}`;
I've watched developers guess MIME types and wonder why their PNG opens as a text file. Match the MIME type to the actual file format. A JPEG is image/jpeg, not image/jpg. That single character difference will cause certain browsers to refuse the download entirely.
Mistake 2: Base64 string includes whitespace or newlines
Some APIs chunk base64 responses across lines. JSON transport can inject \n or \r characters. These invisible characters break the data URL format silently. Always sanitize:
const cleanBase64 = rawBase64.replace(/\s/g, '');
Mistake 3: Using atob() incorrectly for binary files
Here's where things get dangerous. atob() decodes base64 to a binary string — but "binary string" in JavaScript is a misnomer. It's actually a string where each character represents a byte value from 0-255. If you try to handle this like a normal string (passing it through string manipulation, writing it to a text file), you'll corrupt binary data like PDFs, images, and Office documents.
For text files, atob() works fine. For anything binary, you need to go through Uint8Array:
function base64ToBlob(base64, mimeType) {
const byteChars = atob(base64);
const byteNumbers = new Array(byteChars.length);
for (let i = 0; i < byteChars.length; i++) {
byteNumbers[i] = byteChars.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
return new Blob([byteArray], { type: mimeType });
}
Now you have a proper Blob you can create an object URL from, which brings us to the next question.
Should I use a data URL or a Blob URL for downloading base64 files?
Both work, but they behave very differently at scale. Here's the breakdown I give every developer who asks:
Data URLs (data:application/pdf;base64,...) embed the entire file content in the URL string itself. For a 500 KB PDF, that means your href attribute holds roughly 680,000 characters (base64 encoding inflates size by about 33%). The browser keeps this entire string in memory as part of the DOM.
Blob URLs (blob:https://yoursite.com/abc-123) point to a binary blob stored in browser memory. The URL itself is short. The actual file data lives outside the DOM.
For files under 2 MB, data URLs are fine. Quick, simple, no extra steps. But once you cross that threshold — and especially past 10 MB — data URLs start causing real problems. Chrome has a hard limit of 2 MB for data URLs in navigation contexts. Safari starts choking around 5 MB. I've seen a 15 MB base64 PDF crash a mobile Safari tab outright.
The Blob approach is safer for anything substantial:
function downloadBase64AsBlob(base64, mimeType, filename) {
const blob = base64ToBlob(base64, mimeType);
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
Notice that last line: URL.revokeObjectURL(url). If you skip this, the Blob stays in memory until the page unloads. Create enough of these without cleanup and you'll leak memory fast — I tracked down a bug once where a document preview tool was consuming 400 MB after viewing 30 files because nobody revoked the object URLs.
Why doesn't my base64 download work in Safari?
Safari is the final boss of client-side file downloads. Here are the three Safari-specific traps I hit repeatedly:
Trap 1: The download attribute is partially unsupported
Safari historically ignored the download attribute on <a> tags for cross-origin URLs. With data URLs and blob URLs (same-origin), it generally works in modern Safari — but older versions (pre-14) may still navigate to the URL instead of downloading.
The workaround for stubborn Safari versions is to force the download through a Blob with an explicit type:
const blob = new Blob([byteArray], { type: 'application/octet-stream' });
Using application/octet-stream as a fallback MIME type tells Safari "this is arbitrary binary data, don't try to display it." You lose the proper filename extension sometimes, but the download triggers reliably.
Trap 2: Programmatic clicks need user gesture context
If your download is triggered by a setTimeout callback or an async/await chain that lost the user gesture context, Safari will block it. The click must happen within the call stack of a user interaction (like a button press). If you're fetching the base64 data asynchronously after a click, Safari may have already "forgotten" the gesture by the time you call link.click().
The fix: show a loading state, fetch the data, then have the user click a "Download Now" button that triggers the actual download. It's an extra step, but it's the only reliable way to handle async base64 fetches in Safari.
Trap 3: Large data URLs in Safari mobile
Mobile Safari has aggressive memory limits. A 10 MB base64 string in a data URL can crash the tab. Always use the Blob URL approach for mobile, and consider chunking very large files or streaming them through a service worker if you're dealing with files over 50 MB.
How do I set the correct filename and extension when downloading base64?
The download attribute controls the filename, but getting it right requires more thought than you'd expect:
link.download = 'invoice-2024-03.pdf';
Common mistakes I see here:
Missing the file extension. If your base64 is a PDF but you set download="invoice", Windows users get a file with no extension that nothing knows how to open. Always include the extension that matches the MIME type.
Special characters in filenames. If your filename comes from user input or dynamic data, sanitize it. Colons, slashes, and quotes in filenames cause issues across operating systems:
const safeFilename = originalName.replace(/[^\w.-]/g, '_');
Inconsistent casing. .PDF and .pdf are different to some systems. Stick to lowercase extensions unless you have a specific reason not to.
What's the most reliable pattern for base64 file downloads across all browsers?
After debugging this workflow across dozens of projects, here's the battle-tested pattern I recommend. It handles the MIME type correctly, uses Blob URLs for memory efficiency, works in Safari, and cleans up after itself:
function downloadBase64File(base64, mimeType, filename) {
// Strip data URL prefix if present
const base64Data = base64.includes(',')
? base64.split(',')[1]
: base64;
// Decode to binary
const byteChars = atob(base64Data);
const byteArray = new Uint8Array(byteChars.length);
for (let i = 0; i < byteChars.length; i++) {
byteArray[i] = byteChars.charCodeAt(i);
}
// Create Blob and trigger download
const blob = new Blob([byteArray], { type: mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Cleanup after a delay to ensure download started
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
The setTimeout for revocation is deliberate — revoking too quickly can interrupt the download in some browsers. One second is enough for the download to begin without holding memory unnecessarily long.
One last thing: test with real files, not just small text files. A 2 KB text file will pass every test while a 8 MB PDF silently fails in production. Generate a real binary file at the size your users will actually encounter, run it through your code, and verify it opens correctly. That single step catches more bugs than any other testing I know of in this workflow.
Frequently Asked Questions
How do I convert a Base64 string to a downloadable file in JavaScript?
You can convert a Base64 string to a downloadable file by creating a Blob from the decoded bytes and generating an object URL. Simply create an anchor element, set its href to the object URL, assign the download attribute, and programmatically click it to trigger the file download.
How can I download a Base64 Data URL without a backend server?
Downloading a Base64 Data URL entirely client-side is possible by using the browser's Fetch API or the atob() function to decode the string. Once decoded into a Blob, you can use URL.createObjectURL() to create a temporary link that allows the user to save the file directly to their device.
How do I convert a Base64 string to a Blob in the browser?
To convert Base64 to a Blob, first decode the string using atob(), then write the raw binary data into a Uint8Array. Finally, pass that typed array into the Blob constructor along with the appropriate MIME type to create your downloadable file object.
How do I download a Base64 encoded PDF file client-side?
You can download a Base64 encoded PDF by decoding the string and wrapping it in a Blob with the application/pdf MIME type, then creating an object URL. Triggering a click event on an anchor tag pointing to this URL will open or download the PDF without needing a server.
Is it possible to save a Base64 image as a file on the user's device?
Yes, you can save a Base64 image by decoding the string and wrapping it in a Blob with the correct image MIME type, such as image/png or image/jpeg. By generating an object URL and using the download attribute on an anchor tag, the browser will save the image directly to the user's downloads folder.
What is the difference between using a Data URL and a Blob URL for downloading?
A Data URL embeds the entire Base64 encoded file directly within the URL string, which can be inefficient and memory-heavy for large files. A Blob URL acts as a pointer to raw binary data stored in the browser's memory, making it much faster and more reliable for downloading large files client-side.
How do I extract the MIME type from a Base64 Data URL?
A Base64 Data URL typically starts with a prefix like data:image/png;base64, which contains the file's MIME type. You can easily extract this by splitting the string at the first comma and parsing the metadata in the first half to get the specific file type needed for your Blob constructor.
Are there file size limits when downloading Base64 files in a browser?
Yes, browsers have memory limitations, and converting very large Base64 strings to Blobs can cause performance issues or browser crashes. It is generally recommended to avoid Base64 for files larger than a few megabytes and stick to direct binary downloads if possible.
How do I trigger a file download automatically in JavaScript?
To automatically trigger a download, create an invisible anchor element in the DOM and set its href property to your Blob or Data URL. Add the download attribute with your desired file name, append the element to the document body, call its click() method, and then remove it from the DOM.