Base64 Encoding & Decoding in JavaScript
JavaScript provides built-in btoa() and atob() functions for Base64. However, they only handle Latin-1 characters — Unicode strings require a different approach. Here's everything you need.
Basic: btoa() and atob()
The browser (and modern Node.js v16+) provides two global functions:
btoa(string)— binary string to Base64 (encode)atob(base64)— Base64 to binary string (decode)
// Encode
const encoded = btoa("Hello, world!");
console.log(encoded); // "SGVsbG8sIHdvcmxkIQ=="
// Decode
const decoded = atob("SGVsbG8sIHdvcmxkIQ==");
console.log(decoded); // "Hello, world!"
Tip: The name btoa stands for "binary to ASCII" and atob for "ASCII to binary" — the naming is confusing but historical.
Handling Unicode Strings
The biggest pitfall: btoa() throws an InvalidCharacterError for any character with a code point above 255 (emoji, non-Latin scripts, etc.).
btoa("Hello 🌍"); // ❌ InvalidCharacterError
The correct approach is to encode to UTF-8 bytes first, then convert those bytes to a binary string:
// Encode Unicode string to Base64
function encodeBase64(str) {
const bytes = new TextEncoder().encode(str);
const binary = String.fromCharCode(...bytes);
return btoa(binary);
}
// Decode Base64 to Unicode string
function decodeBase64(base64) {
const binary = atob(base64);
const bytes = Uint8Array.from(binary, c => c.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
console.log(encodeBase64("Hello 🌍")); // "SGVsbG8g8J+MjQ=="
console.log(decodeBase64("SGVsbG8g8J+MjQ==")); // "Hello 🌍"
For large strings, spread syntax (...) may hit stack limits. Use a loop instead:
function encodeBase64Safe(str) {
const bytes = new TextEncoder().encode(str);
let binary = '';
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
Node.js with Buffer
In Node.js, the Buffer class is the idiomatic way to work with Base64. It handles Unicode automatically:
// Encode string to Base64
const encoded = Buffer.from("Hello, world! 🌍").toString("base64");
console.log(encoded); // "SGVsbG8sIHdvcmxkISDwn4yN"
// Decode Base64 to string
const decoded = Buffer.from("SGVsbG8sIHdvcmxkISDwn4yN", "base64").toString("utf8");
console.log(decoded); // "Hello, world! 🌍"
You can also encode binary files:
const fs = require("fs");
// Encode file to Base64
const fileBuffer = fs.readFileSync("image.png");
const base64 = fileBuffer.toString("base64");
// Decode Base64 back to file
const decoded = Buffer.from(base64, "base64");
fs.writeFileSync("output.png", decoded);
Note: btoa() and atob() are also available globally in Node.js 16+ if you prefer consistency between browser and server code.
URL-Safe Base64
Standard Base64 contains + and / which are special characters in URLs. URL-safe Base64 (RFC 4648 §5) replaces them with - and _:
// Encode to URL-safe Base64
function toBase64URL(str) {
return encodeBase64(str)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, ""); // remove padding
}
// Decode URL-safe Base64
function fromBase64URL(base64url) {
// Restore standard Base64
let base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
// Add padding if needed
while (base64.length % 4) base64 += "=";
return decodeBase64(base64);
}
// In Node.js:
const encoded = Buffer.from("Hello").toString("base64url"); // Node 16+
const decoded = Buffer.from(encoded, "base64url").toString("utf8");
Encoding Binary / ArrayBuffer
To encode raw binary data (e.g., from a crypto operation or WebSocket message):
// ArrayBuffer or Uint8Array to Base64
function arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = "";
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
// Base64 to ArrayBuffer
function base64ToArrayBuffer(base64) {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
Encoding Files in the Browser
Use the FileReader API to read a file as a Base64 data URI:
function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result); // data:mime;base64,...
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
// Usage with file input
input.addEventListener("change", async (e) => {
const dataUri = await fileToBase64(e.target.files[0]);
console.log(dataUri); // "data:image/png;base64,iVBOR..."
});
Or with the modern arrayBuffer() method:
async function fileToBase64Modern(file) {
const buffer = await file.arrayBuffer();
return arrayBufferToBase64(buffer);
}
FAQ
Why does btoa() throw "InvalidCharacterError"?
btoa() only accepts characters with char codes 0–255 (Latin-1). Anything outside that range — emoji, Chinese characters, mathematical symbols — throws an error. Use the TextEncoder approach or Node.js Buffer to handle Unicode correctly.
Is there a performance difference between btoa() and Buffer?
For typical string sizes, the difference is negligible. For large binary data (megabytes), Node.js Buffer is faster because it is implemented in native C++. In the browser, the loop-based approach avoids stack overflow for very large inputs.
How do I check if a string is valid Base64?
function isBase64(str) {
try {
return btoa(atob(str)) === str;
} catch {
return false;
}
}
Test your Base64 strings instantly
Paste any JavaScript Base64 output into base64.dev to verify it decodes correctly.
Open base64.dev →