Base64 Encoding in JavaScript: btoa(), atob(), and Beyond
JavaScript has built-in functions for Base64 encoding and decoding that work in both browsers and Node.js. But if you've ever tried to encode a string with special characters or emoji, you've probably run into some frustrating errors. This guide covers everything from the basics to handling edge cases properly.
The Basics: btoa() and atob()
The two core functions are btoa() (binary to ASCII) and atob()(ASCII to binary). They've been available in browsers since Internet Explorer 10 and in Node.js since version 16.
// Encoding a simple string
const encoded = btoa("Hello, World!");
console.log(encoded); // "SGVsbG8sIFdvcmxkIQ=="
// Decoding back
const decoded = atob("SGVsbG8sIFdvcmxkIQ==");
console.log(decoded); // "Hello, World!"⚠️ Common Pitfall
btoa()throws an error if you pass a string containing characters outside the Latin-1 range (U+0000 to U+00FF). This means emoji, Chinese characters, and even accented letters like é will cause a DOMException.
Handling Unicode and Emoji
To encode Unicode text (including emoji), you need to convert the string to UTF-8 bytes first, then encode those bytes to Base64. Here's the standard approach:
// Encode Unicode string to Base64
function encodeUnicode(str) {
// Convert string to UTF-8 bytes
const bytes = new TextEncoder().encode(str);
// Convert bytes to binary string for btoa()
let binary = "";
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
// Decode Base64 back to Unicode string
function decodeUnicode(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 new TextDecoder().decode(bytes);
}
// Example with emoji
const original = "Hello 👋 World 🌍";
const encoded = encodeUnicode(original);
console.log(encoded); // "SGVsbG8g8J+RiyBXb3JsZCDwn4yN"
const decoded = decodeUnicode(encoded);
console.log(decoded); // "Hello 👋 World 🌍"Encoding Binary Data (ArrayBuffer)
When working with files, images, or any binary data, you'll typically have an ArrayBuffer or Uint8Array. Here's how to encode that to Base64:
// Convert ArrayBuffer to Base64 string
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);
}
// Convert Base64 string back 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;
}
// Example: encoding a small image
fetch("/icon.png")
.then((res) => res.arrayBuffer())
.then((buffer) => {
const base64 = arrayBufferToBase64(buffer);
const dataUri = `data:image/png;base64,${base64}`;
console.log("Data URI length:", dataUri.length);
});Base64 in Node.js
Node.js provides the same btoa() and atob() functions (since v16), but also offers Buffer which is often more convenient:
const { Buffer } = require("buffer");
// Encode string to Base64
const encoded = Buffer.from("Hello, Node.js!").toString("base64");
console.log(encoded); // "SGVsbG8sIE5vZGUuanMh"
// Decode Base64 to string
const decoded = Buffer.from(encoded, "base64").toString("utf-8");
console.log(decoded); // "Hello, Node.js!"
// Encode a file to Base64
const fs = require("fs");
const imageBuffer = fs.readFileSync("photo.jpg");
const imageBase64 = imageBuffer.toString("base64");
const dataUri = `data:image/jpeg;base64,${imageBase64}`;
// Decode Base64 and save to file
const base64String = "/9j/4AAQSkZJRg..."; // truncated example
const decodedBuffer = Buffer.from(base64String, "base64");
fs.writeFileSync("decoded-photo.jpg", decodedBuffer);URL-Safe Base64 in JavaScript
Standard Base64 uses + and / characters which need URL-encoding. For JWT tokens and URL parameters, use URL-safe Base64:
function toUrlSafe(base64) {
return base64
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
function fromUrlSafe(urlSafe) {
// Add padding back
let base64 = urlSafe.replace(/-/g, "+").replace(/_/g, "/");
while (base64.length % 4 !== 0) {
base64 += "=";
}
return base64;
}
// Example
const original = "Hello, World!";
const standard = btoa(original);
const urlSafe = toUrlSafe(standard);
console.log(standard); // "SGVsbG8sIFdvcmxkIQ=="
console.log(urlSafe); // "SGVsbG8sIFdvcmxkIQ" (no padding)
// Decode URL-safe back
const restored = fromUrlSafe(urlSafe);
console.log(atob(restored)); // "Hello, World!"Performance Considerations
For large data, the naive approach of building a binary string withString.fromCharCode()in a loop can be slow. Here's a faster approach using String.fromCharCode.apply():
// Faster ArrayBuffer to Base64 for large data
function fastArrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
// Process in chunks to avoid stack overflow with apply()
const chunkSize = 8192;
let binary = "";
for (let i = 0; i < bytes.length; i += chunkSize) {
const chunk = bytes.subarray(i, i + chunkSize);
binary += String.fromCharCode.apply(null, chunk);
}
return btoa(binary);
}
// For Node.js, Buffer is already optimized
const base64 = Buffer.from(largeBuffer).toString("base64");Real-World Use Cases
- Canvas to image: Convert canvas content to a Base64 data URI using
canvas.toDataURL()for saving or sharing. - File upload preview: Read a file with
FileReaderand display it as a data URI before uploading. - JWT token decoding: Decode the payload of a JWT token (which uses URL-safe Base64) to inspect its contents on the client side.
- Web Crypto API: Export cryptographic keys as Base64 strings for storage or transmission.
- Local storage: Store small binary blobs (like user avatars) in localStorage as Base64 strings.
Common Mistakes to Avoid
- Using btoa() directly on user input: Always handle Unicode properly with TextEncoder/TextDecoder.
- Forgetting padding: Standard Base64 strings should have length divisible by 4. URL-safe Base64 strips padding, so add it back before decoding.
- Storing large data in localStorage: localStorage has a 5-10MB limit. Base64 adds 33% overhead, so a 3MB file becomes 4MB encoded.
- Not handling errors: Always wrap
atob()in a try-catch when decoding user-provided data.
Conclusion
JavaScript's Base64 support is straightforward for basic use cases, but requires careful handling for Unicode, binary data, and URL-safe encoding. The TextEncoder/TextDecoder API and Node.js's Buffer class are your best friends for robust implementations.
Try our Base64 Encoder & Decoder to quickly test your Base64 strings, or use the URL-Safe Encoder for JWT tokens and URL parameters.