Browsers have had btoa() and atob() since the early days, and modern engines add Uint8Array.prototype.toBase64() and Uint8Array.fromBase64(). The old functions work on “binary strings” where each character is one byte, which is the source of the most common JavaScript Base64 bug.
Encode a string to Base64
btoa('Hello, World!'); // "SGVsbG8sIFdvcmxkIQ=="
This only works for characters with code points up to 255. Anything else throws InvalidCharacterError:
btoa('€'); // ❌ DOMException: The string contains characters outside of the Latin1 range
Unicode-safe encoding
Convert the string to UTF-8 bytes first:
function encodeUtf8(str) {
const bytes = new TextEncoder().encode(str);
let bin = '';
for (const b of bytes) bin += String.fromCharCode(b);
return btoa(bin);
}
encodeUtf8('Grüße, 世界 👋'); // "R3LDvMOfZSwg5LiW55WMIPCfkYs="
In engines that support it (Chrome 140+, Firefox 133+, Safari 18.2+), it is simply:
new TextEncoder().encode('Grüße, 世界 👋').toBase64();
Decode Base64 to a string
function decodeUtf8(b64) {
const bin = atob(b64);
const bytes = Uint8Array.from(bin, (c) => c.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
atob ignores whitespace but throws on any other invalid character. Use new TextDecoder('utf-8', { fatal: true }) if you want an exception for invalid UTF-8 instead of replacement characters (�).
Base64URL
const toUrl = (b64) => b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const fromUrl = (s) => s.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(s.length / 4) * 4, '=');
// Native, where available:
bytes.toBase64({ alphabet: 'base64url', omitPadding: true });
Uint8Array.fromBase64(s, { alphabet: 'base64url' });
Encode a file or image (FileReader)
input.addEventListener('change', () => {
const reader = new FileReader();
reader.onload = () => console.log(reader.result); // "data:image/png;base64,iVBOR..."
reader.readAsDataURL(input.files[0]);
});
To get only the Base64 part, split on the first comma: reader.result.split(',')[1].
Decode Base64 to a downloadable file
function downloadBase64(b64, name, type = 'application/octet-stream') {
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
const url = URL.createObjectURL(new Blob([bytes], { type }));
const a = Object.assign(document.createElement('a'), { href: url, download: name });
a.click();
setTimeout(() => URL.revokeObjectURL(url), 10_000);
}
Prefer Blob URLs over long data: URLs for anything larger than a few hundred kilobytes: they are faster and browsers block navigation to most data: URLs.
Fetch an image and convert it to Base64
const res = await fetch('/logo.png');
const bytes = new Uint8Array(await res.arrayBuffer());
const b64 = bytes.toBase64 ? bytes.toBase64() : btoa(String.fromCharCode(...bytes)); // spread is fine for small files only
For large arrays, build the binary string in chunks (as in encodeUtf8 above) to avoid “Maximum call stack size exceeded”.
Common errors
| Error | Cause | Fix |
|---|---|---|
InvalidCharacterError in btoa |
Non-Latin1 characters | Encode to UTF-8 first |
InvalidCharacterError in atob |
-, _, %3D, or a data URI prefix |
Convert Base64URL, URL-decode, strip data:..., |
Garbled text like é |
Decoded bytes were treated as Latin1 | Decode with TextDecoder |
For Node.js-specific APIs (Buffer, streams), see Base64 in Node.js. To check your results, use the online Base64 decoder.
Comments (0)
Questions, ideas or a bug in “Base64 in JavaScript”? Leave a comment, rate the tool, or reply to others.
No comments yet. Be the first to share your feedback!