How to Base64 Encode and Decode in JavaScript

JavaScript has two Base64 APIs and neither one does what you probably want by default. Browser btoa throws on Unicode, Node Buffer doesn't exist in browsers, and URL-safe Base64 is its own dialect. This guide walks through the right way to encode and decode Base64 in modern JavaScript — text, Unicode, files, URL-safe — with copy-ready code for both the browser and Node. Test any result against the Base64 encoder/decoder tool to confirm parity.

The Two APIs

Modern JavaScript has two ways to do Base64, and which one you reach for depends on where your code runs:

  • Browser: btoa() and atob() — built into every browser since IE10. ASCII only by default; needs help for Unicode.
  • Node.js: Buffer.from() with "base64" encoding — handles bytes natively, no Unicode caveats.

The function names btoa and atob stand for "binary to ASCII" and "ASCII to binary" — confusing, because Base64 is the opposite (Base64 makes binary data ASCII-safe). Modern alternatives have been proposed (Uint8Array.fromBase64) but aren't yet widely shipped. For now, the patterns below are what you write.

Encoding and Decoding ASCII Text (Browser)

// Encode
const encoded = btoa("hello world");
// "aGVsbG8gd29ybGQ="

// Decode
const decoded = atob("aGVsbG8gd29ybGQ=");
// "hello world"

This is the happy path. Works as long as every character is in the Latin-1 range (code points 0–255).

Encoding and Decoding Unicode (Browser)

The first time you hit a non-ASCII character, btoa throws:

btoa("héllo");
// Uncaught DOMException: Failed to execute 'btoa' on 'Window':
// The string to be encoded contains characters outside of the Latin1 range.

The fix is to convert the string to UTF-8 bytes first with TextEncoder, then turn those bytes into a binary string that btoa can accept:

function utf8ToBase64(str) {
  const bytes = new TextEncoder().encode(str);
  // Convert bytes to a Latin-1 string so btoa can swallow it
  let binary = '';
  for (let i = 0; i < bytes.byteLength; i++) {
    binary += String.fromCharCode(bytes[i]);
  }
  return btoa(binary);
}

function base64ToUtf8(b64) {
  const binary = atob(b64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return new TextDecoder().decode(bytes);
}

utf8ToBase64("héllo 👋");
// "aMOpbGxvIPCfkYs="
base64ToUtf8("aMOpbGxvIPCfkYs=");
// "héllo 👋"

The roundabout dance through a "binary string" is unfortunate but necessary in current browsers. Future Uint8Array.fromBase64 will eliminate it.

Encoding and Decoding in Node.js

// Encode any string (Unicode-safe by default)
const encoded = Buffer.from("héllo 👋", "utf-8").toString("base64");
// "aMOpbGxvIPCfkYs="

// Decode back to a string
const decoded = Buffer.from("aMOpbGxvIPCfkYs=", "base64").toString("utf-8");
// "héllo 👋"

// Encode raw bytes (e.g., a binary file you've already read)
const fileBytes = await fs.promises.readFile("./image.png");
const fileB64 = fileBytes.toString("base64");

// Decode Base64 back to a file
await fs.promises.writeFile("./out.png", Buffer.from(fileB64, "base64"));

Buffer never throws on Unicode, doesn't need a TextEncoder wrapper, and works with any input that has a byte representation. It's the easiest API in any language for Base64.

URL-Safe Base64

Standard Base64's +, /, and = characters all have special meaning in URLs. URL-safe Base64 (RFC 4648 §5) replaces them — used by JWTs, OAuth tokens, and most modern web APIs:

// Standard Base64 → URL-safe Base64
function toUrlSafe(b64) {
  return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

// URL-safe Base64 → standard Base64 (add padding back so atob accepts it)
function fromUrlSafe(b64) {
  return b64.replace(/-/g, '+').replace(/_/g, '/') +
         '='.repeat((4 - b64.length % 4) % 4);
}

const safe = toUrlSafe(btoa("hello?world"));
// "aGVsbG8/d29ybGQ" → wait that's the input, after toUrlSafe: "aGVsbG8_d29ybGQ"
atob(fromUrlSafe(safe));
// "hello?world"

In Node, Buffer supports "base64url" directly since Node 16:

Buffer.from("hello?world").toString("base64url");
// "aGVsbG8_d29ybGQ"
Buffer.from("aGVsbG8_d29ybGQ", "base64url").toString();
// "hello?world"

Encoding Files in the Browser

The easiest way to Base64-encode a file in the browser is FileReader.readAsDataURL, which gives you a complete data URL (Base64 included):

fileInput.addEventListener('change', e => {
  const file = e.target.files[0];
  const reader = new FileReader();
  reader.onload = () => {
    // reader.result is a data URL: "data:image/png;base64,iVBOR..."
    const dataUrl = reader.result;
    const base64 = dataUrl.split(',')[1];   // strip the prefix
    console.log('Base64:', base64);
  };
  reader.readAsDataURL(file);
});

For binary processing without the data-URL prefix, use readAsArrayBuffer and convert the bytes yourself:

async function fileToBase64(file) {
  const buffer = await file.arrayBuffer();
  const bytes = new Uint8Array(buffer);
  let binary = '';
  for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
  return btoa(binary);
}

For files larger than a few megabytes, do this work inside a Web Worker so it doesn't freeze the main thread — JavaScript Base64 encoding is CPU-bound and scales linearly with file size.

Common Pitfalls

Padding handling

Base64 strings end in 0, 1, or 2 = characters depending on input length. Most decoders require correct padding; some accept missing padding. JWTs always strip padding, so when you decode a JWT segment you need to add it back: str + '='.repeat((4 - str.length % 4) % 4).

Line breaks

Older Base64 implementations (PEM files, email MIME) wrap output at 76 characters with \n line breaks. atob ignores whitespace, but some strict decoders don't. Strip line breaks before decoding: atob(b64.replace(/\s/g, '')).

Wrong characters for the variant

Standard Base64 fed to a URL-safe decoder produces gibberish, and vice versa. If the Base64 contains - or _, it's URL-safe; if it contains + or /, it's standard. Normalize before decoding.

Double encoding

If you base64-encode an already-base64 string, the result is twice as long and decodes to the base64 string, not the original. Common when storing Base64 in a JSON field then accidentally re-encoding the JSON. The fix is awareness, not code.

Try It Live

Use the Base64 encoder/decoder to verify any output from the snippets above — same input through the tool should produce the same Base64. The What is Base64 guide covers the format itself, the encoding table, and the bigger picture of when to use Base64 versus other binary-safe encodings.

Frequently Asked Questions

What is the difference between btoa and Buffer for Base64 in JavaScript?

btoa and atob are browser-only functions that encode and decode Base64 strings, but they only accept ASCII input — calling btoa("é") throws an InvalidCharacterError. Buffer is a Node-only API that handles arbitrary bytes including Unicode without complaint: Buffer.from("é", "utf-8").toString("base64") just works. In modern browser code you typically wrap btoa with TextEncoder to convert Unicode strings to bytes first; in Node you skip btoa entirely and use Buffer. For code that runs in both environments, feature-detect or write two paths.

Why does btoa throw an InvalidCharacterError on emoji or accented characters?

btoa only accepts characters in the Latin-1 range (code points 0–255). Anything outside that — emoji, Chinese characters, accented Latin letters in some encodings — throws InvalidCharacterError because btoa has no way to convert them to bytes. The fix is to encode the string to UTF-8 bytes first with TextEncoder, then convert those bytes to a binary string that btoa can accept: btoa(String.fromCharCode(...new TextEncoder().encode(str))). For decoding, reverse the process with TextDecoder.

How do I Base64-encode a file in the browser?

Use FileReader.readAsDataURL, which gives you a data URL with the Base64 already in it: const reader = new FileReader(); reader.onload = e => console.log(e.target.result); reader.readAsDataURL(file);. The result is data:image/png;base64,iVBOR... — strip the prefix to get just the Base64 if needed. For binary processing, use readAsArrayBuffer and convert the bytes manually with btoa(String.fromCharCode(...new Uint8Array(buffer))). For very large files (multi-MB), prefer streaming via the new FileReaderSync in Web Workers or process chunks with Blob.slice.

What is URL-safe Base64 and when should I use it?

Standard Base64 uses + and / as two of its 64 characters, plus = for padding. All three have special meaning in URLs and filenames. URL-safe Base64 (RFC 4648 §5) substitutes - for +, _ for /, and usually omits the = padding. Use it any time the encoded string goes into a URL, a query parameter, a JWT (JWTs are always URL-safe Base64), a filename, or a CSS class name. Convert standard to URL-safe in JavaScript with str.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "").

How do I check if a string is valid Base64?

A complete check tries to decode it and catches the exception, because Base64 has subtle rules (length must be a multiple of 4 after padding, only specific characters allowed, padding only at the end). In the browser: function isBase64(s) { try { atob(s); return true; } catch { return false; } }. For URL-safe variants, normalize first by replacing - with +, _ with /, and padding to a multiple of 4 with =. A regex like /^[A-Za-z0-9+/]*={0,2}$/ catches obvious garbage but won't catch all invalid-length strings — pair it with the decode attempt for a full check.