Skip to content
snip tools

Base64 encode and decode in JavaScript (UTF-8 safe)

A snip.tools guide · runs alongside the Base64 encode / decode

Prefer to just run it? Use the free Base64 encode / decode — no install, in your browser.

Open the Base64 encode / decode

JavaScript has had Base64 built in for years — btoa() to encode, atob() to decode. The catch is that they predate Unicode-aware strings and quietly break the moment your text contains an accent or an emoji. Here's why, the few lines that fix it for good, and a more forgiving decoder for real-world input. (It's the same approach our in-browser Base64 encoder & decoder uses.)

Why btoa() and atob() aren't enough

btoa ("binary to ASCII") expects a string where every character fits in one byte — the Latin-1 range. Pass it anything outside that range and it throws:

// The built-ins — fine for ASCII, broken for everything else:
btoa('hello');        // "aGVsbG8="   ✅
atob('aGVsbG8=');     // "hello"      ✅

btoa('héllo');        // ❌ InvalidCharacterError
btoa('🚀 launch');    // ❌ throws — emoji aren't Latin-1

The fix isn't to abandon btoa/atob — they're the right low-level primitives. It's to convert your text to UTF-8 bytes first, so every character is represented as bytes btoa can handle, and to reverse that on the way back.

UTF-8-safe encoding

TextEncoder turns a string into its UTF-8 bytes. Pack those bytes into a binary string, then Base64 it:

// Encode any string (UTF-8 safe) → Base64
function base64Encode(str) {
  const bytes = new TextEncoder().encode(str); // string → UTF-8 bytes
  let binary = '';
  for (const b of bytes) binary += String.fromCharCode(b);
  return btoa(binary);
}

base64Encode('🚀 launch'); // "8J+agCBsYXVuY2g="

UTF-8-safe decoding

Decoding is the mirror image: atob gives you a binary string, you read its char codes back into a byte array, and TextDecoder turns those UTF-8 bytes back into a proper string:

// Decode Base64 → string (UTF-8 safe)
function base64Decode(b64) {
  const binary = atob(b64);
  const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
  return new TextDecoder().decode(bytes);
}

base64Decode('8J+agCBsYXVuY2g='); // "🚀 launch"

These two functions round-trip any string — accents, CJK characters, emoji — correctly, because the bytes in the middle are real UTF-8.

A tolerant decoder for real-world input

Base64 you receive from the wild isn't always clean. It may arrive wrapped across lines (email bodies), in the URL-safe alphabet that swaps +// for -/_ (used by JWTs), or with its = padding stripped. A strict atob chokes on all three. A few .replace() calls make the decoder forgiving:

// A forgiving decoder: handles line-wrapped input,
// the URL-safe alphabet (-/_), and missing padding.
function base64DecodeTolerant(b64) {
  let s = b64
    .replace(/\s+/g, '')   // strip newlines/spaces (wrapped email bodies)
    .replace(/-/g, '+')    // URL-safe → standard
    .replace(/_/g, '/');
  const rem = s.length % 4;
  if (rem) s += '='.repeat(4 - rem); // restore padding
  const bytes = Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
  return new TextDecoder().decode(bytes);
}

This normalises the input before decoding, so a JWT segment or a line-wrapped blob decodes cleanly while genuinely invalid data still throws — exactly what you want. Note that Base64 has no checksum, so a string that was never valid Base64 will simply decode to garbage rather than error.

On the server: Node.js

In Node you don't need any of the above — the Buffer API is UTF-8 safe out of the box:

// Node.js: the Buffer API is simplest and UTF-8 safe by default.
const b64 = Buffer.from('🚀 launch', 'utf8').toString('base64');
const txt = Buffer.from(b64, 'base64').toString('utf8');

Use Buffer on the server and the TextEncoder/TextDecoder pair in the browser. One last reminder that applies everywhere: Base64 is an encoding, not encryption — it hides nothing and anyone can decode it, so never use it to protect secrets. For putting data into a URL rather than a byte stream, reach for URL encoding instead.

Try it now: the Base64 encode / decode does everything in this guide in your browser — nothing is uploaded. Browse more guides or the full tool list.