btoa e atob: só Latin-1
O btoa (“binary to ASCII”) codifica uma string em que cada caractere é tratado como um byte. O atob faz o inverso. Isso funciona com ASCII:
btoa("hello"); // "aGVsbG8="
atob("aGVsbG8="); // "hello"
E quebra com qualquer coisa fora dos code points 0–255:
btoa("héllo ✓");
// InvalidCharacterError: Failed to execute 'btoa' on 'Window':
// The string to be encoded contains characters outside of the Latin1 range.
Pior: caracteres que cabem no Latin-1 são codificados sem erro, mas como os bytes errados. btoa("héllo") retorna aOlsbG8= (é em Latin-1 = 0xE9), enquanto todas as outras linguagens e o codificador Base64 geram a versão UTF-8 aMOpbGxv.
O btoa e o atob também existem no Node.js 16+ como globais legados, mas no servidor a API Buffer é mais rápida e tem a codificação base64url embutida. Veja Base64 em Node.js.
Codificar e decodificar strings UTF-8
A solução portável é transformar a string em bytes UTF-8 com TextEncoder e passar esses bytes ao btoa como uma “string binária”:
function encodeUtf8(str) {
const bytes = new TextEncoder().encode(str);
let bin = "";
for (const b of bytes) bin += String.fromCharCode(b);
return btoa(bin);
}
function decodeUtf8(b64) {
const bin = atob(b64);
const bytes = Uint8Array.from(bin, c => c.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
encodeUtf8("héllo wörld"); // "aMOpbGxvIHfDtnJsZA=="
decodeUtf8("aMOpbGxvIHfDtnJsZA=="); // "héllo wörld"
atob("aMOpbGxvIHfDtnJsZA=="); // "héllo wörld" (no TextDecoder)
O velho truque btoa(unescape(encodeURIComponent(str))) dá o mesmo resultado, mas depende do unescape, que está obsoleto.
Uint8Array.toBase64 e fromBase64
A API mais nova trabalha direto com bytes, sem a etapa da string binária. Ela é suportada no Chrome e no Edge 140+, no Firefox 133+ e no Safari 18.2+.
const bytes = new TextEncoder().encode("héllo wörld");
bytes.toBase64(); // "aMOpbGxvIHfDtnJsZA=="
new TextDecoder().decode(Uint8Array.fromBase64("aMOpbGxvIHfDtnJsZA=="));
// "héllo wörld"
Há também o setFromBase64, que decodifica para um buffer existente e informa quanto usou:
const target = new Uint8Array(8);
target.setFromBase64("aGVsbG8K"); // { read: 8, written: 6 }
Detecte o recurso antes de depender dele: typeof Uint8Array.prototype.toBase64 === "function". O guia do Uint8Array.toBase64 cobre todas as opções.
Base64 URL-safe com e sem preenchimento
Com a API nova, passe opções:
const d = new TextEncoder().encode("<<???>>");
d.toBase64({ alphabet: "base64url" }); // "PDw_Pz8-Pg=="
d.toBase64({ alphabet: "base64url", omitPadding: true }); // "PDw_Pz8-Pg"
Uint8Array.fromBase64("PDw_Pz8-Pg", { alphabet: "base64url" }); // bytes of "<<???>>"
Com btoa/atob, traduza o alfabeto você mesmo:
const toB64Url = b64 =>
b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
const fromB64Url = s => {
s = s.replace(/-/g, "+").replace(/_/g, "/");
return s + "=".repeat((4 - s.length % 4) % 4);
};
toB64Url(btoa("<<???>>")); // "PDw_Pz8-Pg"
atob(fromB64Url("PDw_Pz8-Pg")); // "<<???>>"
É isso que os JWTs usam; veja Base64 vs Base64URL ou cole um token no decodificador de JWT.
Arquivos, Blobs e dados grandes
Para ler um arquivo escolhido pelo usuário, o FileReader.readAsDataURL retorna uma data URI. Remova tudo até a vírgula para ter o Base64 puro:
const reader = new FileReader();
reader.onload = () => {
const dataUrl = reader.result; // "data:text/plain;base64,aGVsbG8K"
const b64 = dataUrl.slice(dataUrl.indexOf(",") + 1);
};
reader.readAsDataURL(file);
Com a API nova fica mais curto: new Uint8Array(await file.arrayBuffer()).toBase64().
Para transformar o Base64 de volta num arquivo para download, decodifique para bytes e envolva-os num Blob:
const bytes = Uint8Array.fromBase64(b64);
const url = URL.createObjectURL(new Blob([bytes], { type: "application/pdf" }));
Com buffers grandes, nunca os espalhe em String.fromCharCode(...bytes). Com 500.000 bytes, o Chromium lança RangeError: Maximum call stack size exceeded. Monte a string binária em fatias:
function bytesToBase64(bytes) {
let bin = "";
for (let i = 0; i < bytes.length; i += 0x8000) {
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
}
return btoa(bin);
}
// 5,000,000 bytes -> 6,666,668 characters
A saída fica cerca de um terço maior que a entrada; o guia de aumento de tamanho traz a fórmula exata.
Validação e mensagens de erro
O atob ignora espaços em branco ASCII e tolera preenchimento ausente, então atob(" aG k= ") e atob("aGk") retornam "hi". Ele rejeita caracteres fora do alfabeto padrão:
atob("PDw_Pz8-Pg");
// InvalidCharacterError: Failed to execute 'atob' on 'Window':
// The string to be decoded is not correctly encoded.
O fromBase64 lança SyntaxError e pode ficar mais rígido com lastChunkHandling:
Uint8Array.fromBase64("PDw_Pz8-Pg");
// SyntaxError: Found a character that cannot be part of a valid base64 string.
Uint8Array.fromBase64("aGl="); // 2 bytes, accepted
Uint8Array.fromBase64("aGl=", { lastChunkHandling: "strict" });
// SyntaxError: The base64 input terminates with non-zero padding bits.
O padrão, "loose", aceita preenchimento ausente. O "strict" exige preenchimento correto e bits de preenchimento zero; o "stop-before-partial" deixa um bloco final incompleto para decodificadores em streaming. O texto das mensagens muda de navegador para navegador, mas os nomes dos erros são padronizados. Para uma conferência visual, o validador de Base64 destaca o problema exato, e erros comuns de Base64 lista as causas.
Perguntas frequentes
Por que o btoa lança InvalidCharacterError?
O btoa só aceita caracteres da faixa Latin-1 (code points 0–255). Qualquer emoji, caractere cirílico ou CJK faz ele lançar o erro. Converta antes a string em bytes UTF-8 com TextEncoder ou use Uint8Array.prototype.toBase64().
Posso usar Uint8Array.toBase64 em produção?
Ele já vem nas versões atuais do Chrome, Edge, Firefox e Safari, mas não em navegadores antigos nem no Node.js 24. Detecte o recurso com typeof Uint8Array.prototype.toBase64 === "function" e caia para o helper com btoa. Detalhes em Uint8Array.toBase64 explicado.
Por que o atob retorna caracteres estranhos como é em vez de é?
O atob retorna um caractere por byte, então uma sequência UTF-8 de dois bytes vira dois caracteres Latin-1. Passe o resultado por Uint8Array.from(bin, c => c.charCodeAt(0)) e TextDecoder para obter o texto correto.
Como decodificar Base64URL com atob?
Troque - por + e _ por / e complete com = até um múltiplo de 4 caracteres. Com a API nova, chame Uint8Array.fromBase64(s, { alphabet: "base64url" }) e pule as duas etapas.