Three lines of code, and it still goes wrong. You call a function, get undefined, and lose an hour. Converting an image to Base64 in JavaScript looks trivial until the async timing or a CORS header bites you.
There are exactly three approaches. This covers all of them, plus the traps that only show up in production.
How to Convert an Image to Base64 in JavaScript
Pick your method by where the image comes from:
- A file input or drag-and-drop — use FileReader.
- An image already on the page — use a canvas.
- A remote URL — use fetch, then FileReader.
Only one of those gives you the original bytes. We will come back to why that matters.
If you just need a string right now and not the code, our image to Base64 converter does it in the browser with no upload.
Method 1: FileReader and readAsDataURL
This is the one you want most of the time. It reads the file straight from disk and encodes it without touching a single pixel.
const input = document.querySelector('input[type=file]');
input.addEventListener('change', (e) => {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = () => {
console.log(reader.result); // data:image/png;base64,iVBORw0...
};
reader.readAsDataURL(file);
});
The whole thing hinges on onload. The read is asynchronous, so anything outside that handler runs before the result exists.
In modern code we wrap it in a Promise and forget it was ever callback-based:
const toBase64 = (file) => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(file);
});
const dataUrl = await toBase64(file);
One caveat we hit on a client project: for files over roughly 10 MB, this locks the main thread while it encodes. Move it into a Web Worker if your users upload large images. FileReader works there too.
Method 2: Reading Pixels with canvas.toDataURL
Use this when the image is already rendered and you want to change format or size on the way out.
function imageToBase64(img, type = 'image/png', quality = 0.92) {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
canvas.getContext('2d').drawImage(img, 0, 0);
return canvas.toDataURL(type, quality);
}
Powerful, and lossy in ways people do not expect. The canvas decodes your image to raw pixels and compresses it again from scratch.
Three things disappear in that round trip:
- EXIF metadata. Camera settings, timestamps, GPS coordinates — all gone.
- The ICC colour profile. Wide-gamut photos can come back visibly flatter.
- Bit depth control. Output is always 32-bit RGBA, so an efficient 8-bit PNG gets larger.
That metadata loss is genuinely useful when you are scrubbing user uploads. It is a disaster when you needed the orientation flag. We have shipped both bugs.
The PNG to Base64 encoder deliberately avoids canvas for exactly this reason.
Method 3: Fetching a Remote Image as a Blob
For an image on another server, fetch it, get a Blob, then hand that to FileReader:
async function urlToBase64(url) {
const res = await fetch(url);
const blob = await res.blob();
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
This keeps the original bytes, unlike the canvas route. It also fails instantly without the right CORS headers.
The CORS Trap That Breaks Two of These
Here is the error that sends developers in circles:
Uncaught DOMException: Failed to execute 'toDataURL' on
'HTMLCanvasElement': Tainted canvases may not be exported.
Drawing a cross-origin image onto a canvas taints it. The browser then refuses to let any script read the pixels back, because otherwise any page could silently exfiltrate images you are logged in to see.
Two things have to line up:
- Set
img.crossOrigin = 'anonymous'before assigningsrc. After is too late. - The remote server must return
Access-Control-Allow-Origin.
The second is not yours to fix. If the server does not cooperate, no client-side trick will help — you need a server-side proxy. Our CORS tester will tell you in seconds whether the headers are actually there.
Which Method Should You Pick?
| Situation | Use | Bytes preserved? |
|---|---|---|
| File input or drop zone | FileReader | Yes |
| Resize or change format | Canvas | No |
| Strip EXIF on purpose | Canvas | No, deliberately |
| Remote URL, CORS allowed | fetch + FileReader | Yes |
| Remote URL, CORS blocked | Server-side proxy | Yes |
| Node.js | Buffer | Yes |
On the server it is one line, and none of the browser complexity applies:
const b64 = require('fs').readFileSync('logo.png').toString('base64');
Mistakes That Have Cost Us Hours
Five we keep seeing in code review:
- Slicing the prefix by length.
data:image/png;base64,anddata:image/jpeg;base64,are different lengths. Split on the first comma. - Calling btoa on text. It throws on anything outside Latin-1. Encode through TextEncoder first — our Base64 encoder and decoder handles that correctly.
- Forgetting revokeObjectURL. If you use createObjectURL for previews, the reference leaks until the page unloads.
- Assuming Base64 is private. It is trivially reversible. Anyone can paste your string into a Base64 image decoder and see the picture.
- Inlining everything. The 33% size penalty is real, and it compounds.
Wrapping Up
To convert an image to Base64 in JavaScript, reach for FileReader first. It is the simplest option and the only browser method that preserves your file exactly.
Use canvas when you deliberately want to transform the image, and remember what the round trip costs. Use fetch for remote files, and check the CORS headers before you write a line of code.
Then ask whether you should inline the result at all. We work through that in when Base64 images are worth it and in our guide to Base64 images in CSS and HTML.
