The names atob and btoa can suggest ordinary text conversion. Their browser contract is narrower: they convert between Base64 text and a string whose character values represent individual bytes. That byte string is a container, not a promise that the result is readable Unicode text.
Two different strings
JavaScript strings use UTF-16 code units. A byte string deliberately restricts each character value to 0–255. btoa('Hello') works because ASCII values fit that range. btoa('你好') fails because those characters do not fit in one byte. Even a character that does fit can be misleading: btoa('é') encodes the single byte E9, not the UTF-8 bytes C3 A9 expected by many services.
console.log(btoa('é')); // 6Q== (byte E9)
console.log(btoa(String.fromCharCode(0xc3, 0xa9))); // w6k= (UTF-8)
Put UTF-8 conversion around the byte functions
TextEncoder produces UTF-8 bytes from text. After decoding Base64, a fatal TextDecoder converts valid UTF-8 bytes back to text and rejects invalid sequences instead of silently replacing them. This small example is deliberately explicit so the boundary is visible.
const text = '中文😊';
const bytes = new TextEncoder().encode(text);
let binary = '';
for (const byte of bytes) binary += String.fromCharCode(byte);
const encoded = btoa(binary);
console.log(encoded); // 5Lit5paH8J+Yig==
const decodedBytes = Uint8Array.from(atob(encoded), c => c.charCodeAt(0));
const decoded = new TextDecoder('utf-8', { fatal: true }).decode(decodedBytes);
console.log(decoded); // 中文😊
Do not treat a binary result as a broken string
atob('/w==') represents the byte FF. It is not valid UTF-8 by itself, but it is perfectly valid binary data. The correct next action may be a byte download or hexadecimal view, not a different text conversion trick. Running that byte string through TextEncoder would encode the character U+00FF and change the bytes.
The browser decoder is also not a complete protocol validator. Accepted whitespace and omitted padding can differ from strict application requirements. If a protocol requires canonical encoding, validate alphabet, padding and unused bits separately. For large files, avoid spreading a complete byte array into String.fromCharCode(...bytes); argument-count limits can fail long before the file becomes unusually large.
Read the browser contracts in MDN: atob() and MDN: btoa(). The application should decide whether it wants a permissive conversion, a strict validator or a file-preserving workflow, rather than assuming those are the same task.