DEV Community

Mubeen Khan
Mubeen Khan

Posted on

btoa() broke my emoji: Base64 in JavaScript done right

btoa() broke my emoji: Base64 in JavaScript done right

Base64 looks trivial until it corrupts your users' text. I learned this the way most people do: a feature that worked perfectly in testing, then a bug report with emoji in it.

The trap: btoa is Latin-1 only

JavaScript's built-in btoa() converts a string to Base64, but it only understands Latin-1 characters (code points 0 to 255). Feed it anything else and it throws, or worse, silently mangles the result:

btoa("Hello"); // SGVsbG8=, fine
btoa("café"); // works, but only by luck (é is Latin-1)
btoa("👋 hello"); // throws: InvalidCharacterError
Enter fullscreen mode Exit fullscreen mode

The same trap catches Spanish accents in some contexts and every emoji, always. The reason is that btoa operates on characters, not bytes, and Base64 is fundamentally a bytes-to-text encoding.

The fix: encode UTF-8 bytes first

The correct pipeline is text to UTF-8 bytes, then bytes to Base64. TextEncoder and TextDecoder do the byte conversion, and they handle every script:

function encodeUtf8(text) {
  const bytes = new TextEncoder().encode(text);
  let binary = "";
  bytes.forEach((b) => (binary += String.fromCharCode(b)));
  return btoa(binary);
}

function decodeUtf8(base64) {
  const binary = atob(base64);
  const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
  return new TextDecoder().decode(bytes);
}

encodeUtf8("👋 hello"); // 8J+RiiBoZWxsbw==
decodeUtf8("8J+RiiBoZWxsbw=="); // 👋 hello
Enter fullscreen mode Exit fullscreen mode

Two details worth knowing. First, decoding should accept strings with or without the trailing = padding, because many APIs strip it. Second, if you are putting the result in a URL or a JWT, you need the URL-safe alphabet: replace + with - and / with _. That variant is called Base64URL, and it is what tokens actually use.

The one warning that matters

Base64 is not encryption. It is reversible encoding for transport: anyone can decode a Base64 string in a second. Never use it to hide a password, an API key, or anything secret. Its job is moving bytes through text-only systems like JSON payloads, email attachments, and data URIs, not keeping them private.

If you want to try the conversion without writing code, this free Base64 encoder and decoder handles the UTF-8 details, the URL-safe variant, and file-to-Base64 conversion, all in the browser: https://www.theutilityhub.online/tools/base64-encoder-decoder

Try it with your own name in your own script, then decode it back. If it round-trips exactly, your encoding is UTF-8 safe. If it does not, you have found the btoa trap before your users do.

Top comments (0)