DEV Community

Li Yi
Li Yi

Posted on

Base64 encode and decode in the browser — with proper Unicode support

The problem with most Base64 tools

You paste text into an online Base64 encoder, hit a button, get the result. Simple enough — until you try encoding something like 你好世界 or 🎉 and the output comes back as garbage, or the site silently drops characters.

The root cause is almost always the same: the tool converts text to bytes using Latin-1 (a single-byte encoding) instead of UTF-8 (variable-length, covers all of Unicode). The result is a Base64 string that decodes correctly in ASCII contexts but produces mojibake the moment someone reads it back.

Here is how the encoding actually works, what the pitfalls are, and how to verify that any Base64 tool — online or otherwise — handles Unicode correctly.

Encoding: text → bytes → Base64

The pipeline has two steps. First, convert the string to a UTF-8 byte sequence. Second, encode those bytes as Base64.

function encodeBase64(text) {
  const bytes = new TextEncoder().encode(text);
  let binary = '';
  const chunkSize = 0x8000;
  for (let i = 0; i < bytes.length; i += chunkSize) {
    binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
  }
  return btoa(binary);
}
Enter fullscreen mode Exit fullscreen mode

TextEncoder produces a Uint8Array of UTF-8 bytes — 你好 becomes 6 bytes, 🎉 becomes 4. The loop then feeds those bytes to btoa() in chunks. The chunk size (0x8000 = 32768) keeps String.fromCharCode calls small enough to avoid call-stack limits on large inputs.

The naive one-liner many people reach for — btoa(unescape(encodeURIComponent(text))) — works but is slower and harder to reason about. TextEncoder is the modern equivalent and has been available in every browser for years.

Decoding: Base64 → bytes → text

The reverse direction has more ways to fail:

function decodeBase64(input) {
  // Strip whitespace, accept URL-safe alphabet (- and _)
  const compact = input.replace(/\s+/g, '');
  if (compact.length === 0) return { ok: true, output: '' };

  const normalized = compact.replace(/-/g, '+').replace(/_/g, '/');
  const payload = normalized.replace(/=+$/, '');

  if (!/^[A-Za-z0-9+/]*$/.test(payload)) {
    return { ok: false, error: 'Invalid characters in Base64 input' };
  }
  if (payload.length % 4 === 1) {
    return { ok: false, error: 'Invalid Base64 length' };
  }

  let binary;
  try {
    binary = atob(normalized);
  } catch {
    return { ok: false, error: 'Malformed Base64' };
  }

  const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
  try {
    const output = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
    return { ok: true, output };
  } catch {
    return { ok: false, error: 'Decoded bytes are not valid UTF-8' };
  }
}
Enter fullscreen mode Exit fullscreen mode

Three details worth calling out:

1. URL-safe Base64. The standard alphabet uses + and /, but URLs often replace those with - and _. A decoder that does not normalise first will reject valid input or produce wrong output. The replace calls above handle both alphabets transparently.

2. Padding tolerance. Base64 output is padded with = to a multiple of 4 characters. Some generators omit the padding; some add extra newlines. Stripping trailing = and whitespace before decoding avoids both failure modes.

3. The fatal: true flag. Without it, TextDecoder silently replaces invalid byte sequences with the Unicode replacement character U+FFFD. With it, a single bad byte throws, which is what you want — silent corruption is worse than a loud error.

Three quick tests for any Base64 tool

Before you trust an online encoder with real data:

  1. Encode 你好. Correct result: 5L2g5aW9. If the tool throws instead of giving you that, it is calling btoa() on the raw string — btoa rejects anything above U+00FF with InvalidCharacterError.
  2. Encode 🎉. Correct result: 8J+OiQ== (note the two = padding characters — a 4-byte input needs padding to reach a multiple of 4 output characters). If you get a 4-character string without padding, the tool is stripping it.
  3. Decode 5L2g5aW9 back. Must produce 你好 exactly. Round-tripping is the real test — a tool can produce plausible-looking output and still be mapping bytes wrongly.

If a tool fails any of these, it is not handling Unicode. Full stop.

What I shipped

I put this exact pipeline behind a page with bidirectional conversion, URL-safe alphabet support, whitespace tolerance, and clear error messages when the input is not valid Base64 or does not decode to valid UTF-8: ToolStack Base64 Encoder/Decoder.

It is part of a set of 69 browser-local tools — JSON formatting, image compression, colour converters, unit conversion, calculators, QR codes — in 7 languages. No sign-up, nothing uploaded. For the Base64 tool, the entire pipeline is TextEncoder → btoa / atob → TextDecoder, exactly as above.

Disclosure, since I would check it on someone else's page: GA4 page-level analytics is enabled; text input and output never leave the tab, and there is no server-side storage of anything you paste.

https://tools.mindutil.com/

Top comments (0)