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 →