Skip to content
Base64
UTF-8 and URL-safe encode/decode for samples and non-sensitive values.
Blog

Why btoa() Breaks on Unicode, and How to Fix It

btoa() accepts é but not emoji. Decode Y2Fmw6k= as UTF-8 to get café; compare errors and invisible BOM text.

Try the harmless samples →
Three key takeaways

Correction, 10 October 2026: Latin-1 errors are not UTF-8 mojibake. Use harmless samples.

  1. ①
    é: accepted is not UTF-8 Latin-1 Y2Fm6Q== is rejected here. These are not UTF-8 bytes.
  2. ②
    café: the opposite path UTF-8 bytes read as Latin-1 cause this display. Decode Y2Fmw6k= as UTF-8 to get café.
  3. ③
    Invisible BOM: preserved, not cleaned 77u/Y2Fmw6k= includes U+FEFF before café. The tool keeps it and explains what will be copied.
AI-generated concept illustration: a paper marked btoa() breaks on emoji beside a laptop and a yellow cup; not a tool screenshot or an actual conversion result.

An emoji can make btoa() throw. An accented letter can pass without an error—and still produce bytes your UTF-8 decoder cannot read. Those are different failures, and confusing them makes debugging harder.

Correction, 10 October 2026: the earlier version said that decoding the Latin-1 result as UTF-8 produces é. That direction was wrong. A strict UTF-8 decoder rejects the Latin-1 byte; café appears in our example when UTF-8 bytes are treated as Latin-1 characters. The examples below separate the two. We also reproduced and corrected an invisible leading-BOM loss in the text tool.

Why btoa() rejects emoji but accepts é

The browser’s btoa encodes a string whose characters stand for byte values from 0 to 255. It does not automatically turn ordinary JavaScript text into UTF-8.

An emoji such as 🌍 cannot be passed directly: btoa('Hello 🌍') throws InvalidCharacterError. But é (U+00E9) fits inside the byte range. btoa('café') therefore succeeds and returns Y2Fm6Q==. Its last decoded byte is E9, not the UTF-8 sequence C3 A9 for é.

Accepting the input is not the same as choosing the encoding you intended. MDN’s btoa reference distinguishes byte-valued strings from Unicode text.

Same-looking text, different bytes

These invented examples have no newline at the end:

PathBase64Result
btoa('café') → atobY2Fm6Q==café as byte-valued Latin-1 characters
btoa('café') → this UTF-8 text decoderY2Fm6Q==explicit non-UTF-8 error; no text to copy
UTF-8 bytes for café → this decoderY2Fmw6k=café
UTF-8 bytes for café → atob treated as textY2Fmw6k=café

Try the two Base64 values in the English text tool with Decode selected. One returns café; the other reports that its bytes are not valid UTF-8. An error does not mean you need a password. This is a text decoder, not a file or image viewer.

We checked the byte values with Node Buffer, the browser-style btoa/atob APIs and the site’s conversion tests. These checks cover the stated samples—not every possible input or a security audit of your device.

Encode UTF-8 bytes, then decode them as UTF-8

For ordinary Unicode text, first use TextEncoder to get UTF-8 bytes and encode those bytes as Base64. Decode the bytes through a UTF-8 TextDecoder with errors reported rather than replaced.

Hello 🌍 produces SGVsbG8g8J+MjQ== in this tool. Decoding that value returns the same text. A JavaScript string containing an unmatched UTF-16 surrogate is rejected instead of silently becoming �. A literal � that was already part of valid input is a different case and is preserved.

What if an invisible character disappears?

A leading U+FEFF can look like nothing on screen. Its UTF-8 bytes are EF BB BF. The harmless value 77u/Y2Fmw6k= represents U+FEFF followed by café—five Unicode code points, not four.

The previous tool silently removed that first character. The corrected decoder keeps it and displays a BOM notice explaining that copied output includes the invisible character. Re-encoding the preserved text returns 77u/Y2Fmw6k=; it must not quietly become Y2Fmw6k=. This is deliberate preservation, not automatic cleanup of a text file. The Encoding Standard specifies how the decoder’s BOM option controls that behavior.

The output counter is labelled UTF-16 code units, not visible symbols. For example, the moon 🌙 takes two UTF-16 code units, while this BOM-plus-café example takes five. Do not use that counter as a UTF-8 byte count.

URL-safe is an alphabet, not a privacy guarantee

Standard Base64 uses + and /; base64url uses - and _. This tool removes trailing = in its URL-safe encoding mode and accepts missing padding when decoding. Other formats can have different padding requirements, so check the receiving format. RFC 4648 describes the alphabets and padding rules.

The URL-safe form of Hello 🌍 here is SGVsbG8g8J-MjQ. A test JWT viewer can inspect a harmless token’s structure, but this alphabet does not verify a signature or protect the contents. For a complete value destined for a query string, use the URL encoder as required by that URL format.

Before you paste or share

Base64 is reversible encoding, not encryption. This code is not a secret offers another invented example to try. Do not paste production access tokens, passwords or private conversations to investigate them; follow an approved local workflow.

Conversion functions run in the browser. That describes their execution location—not a guarantee about the entire page, clipboard, extensions or device. Use the supplied sample or non-sensitive test values only.

The cover is an AI-generated concept illustration, not a tool screenshot or a photograph of a tested conversion.

Frequently asked questions

Does btoa() reject every accented letter?
No. é fits inside the byte-valued range and btoa('café') returns Y2Fm6Q==. Those are Latin-1 bytes, not UTF-8; the site's UTF-8 decoder rejects this value. An emoji such as 🌍 cannot be passed directly to btoa. Encode ordinary text as UTF-8 bytes first.
Why did my decoded café become café?
In the controlled example, Y2Fmw6k= contains UTF-8 bytes. atob returns byte-valued characters and displays café if they are treated as text. A UTF-8 decoder returns café. This differs from Y2Fm6Q==, whose Latin-1 bytes are invalid UTF-8. Check the original encoding instead of assuming that every corrupted-looking value is fixed the same way.
What is the difference between standard and URL-safe Base64?
Standard Base64 uses + and /; the URL-safe alphabet uses - and _. Padding requirements depend on the receiving format. This tool's URL-safe encoder removes trailing =, and its decoder accepts missing padding. That does not promise that every receiver accepts the same form or that the data is private.
Does Base64 make data larger?
Standard padded Base64 uses four characters for each group of up to three bytes. Its length is 4 × ceil(byte count / 3). The overhead approaches 33% for large inputs, but can be much larger for a short input. Count bytes rather than visible symbols.
Is the text I paste sent to a server?
The Base64 conversion itself runs in your browser. Browser-side processing alone does not guarantee the safety of production tokens or credentials. Use examples or non-sensitive test values only.

Sources

Review record: Codex AI editorial/code audit (not a human specialist review)

Recorded checks and limits

At 2026-10-10 04:24:32 KST, reopened MDN btoa, RFC 4648 and the Encoding Standard. Reproduced the old loss of U+FEFF in the live English tool, then checked exact Latin-1/UTF-8 values, BOM preservation and rejection of malformed Unicode with Node Buffer and targeted tests. Fixed-tool preview/production browser checks are separate release receipts, not this timestamp's claim. Earlier bearer-token safety reference remains dated 2026-09-24. Only the generic emoji-rejection cover is retained; inaccurate or synthetic-result cards are no longer used in this article.

What this guide adds — Separates btoa rejection, Latin-1 bytes rejected by a UTF-8 text decoder, and UTF-8 bytes misread as Latin-1 using exact café examples. Reproduced the old leading-BOM loss in the live English tool with an invented sample, then checked the correction and malformed-surrogate rejection against independent bytes and targeted tests. No secret or general security audit is involved.

This records the stated checks; it is not an independent guarantee of accuracy.

Back to the tool →