Base64 Encoding in JavaScript: btoa(), atob(), and Beyond

Published on February 5, 2026 · 8 min read

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 FileReader and 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.