btoa y atob: solo Latin-1
btoa («binary to ASCII») codifica una cadena tratando cada carácter como un byte. atob hace lo contrario. Con ASCII funciona:
btoa("hello"); // "aGVsbG8="
atob("aGVsbG8="); // "hello"
Falla con cualquier cosa fuera de los puntos de código 0–255:
btoa("héllo ✓");
// InvalidCharacterError: Failed to execute 'btoa' on 'Window':
// The string to be encoded contains characters outside of the Latin1 range.
Peor aún, los caracteres que caben en Latin-1 se codifican sin ningún error, pero con los bytes equivocados. btoa("héllo") devuelve aOlsbG8= (la é de Latin-1 es 0xE9), mientras que cualquier otro lenguaje y el codificador Base64 producen la versión UTF-8, aMOpbGxv.
btoa y atob también existen en Node.js 16+ como globales heredados, pero en el servidor la API Buffer es más rápida y trae una codificación base64url integrada. Consulta Base64 en Node.js.
Codificar y decodificar cadenas UTF-8
La solución portable es convertir la cadena en bytes UTF-8 con TextEncoder y pasar esos bytes a btoa como una «cadena binaria»:
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)
El viejo truco btoa(unescape(encodeURIComponent(str))) da el mismo resultado, pero depende de unescape, que está obsoleto.
Uint8Array.toBase64 y fromBase64
La API nueva trabaja directamente con bytes, sin el paso por una cadena binaria. Está disponible en Chrome y Edge 140+, Firefox 133+ y 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"
También existe setFromBase64, que decodifica sobre un búfer existente e indica cuánto ha usado:
const target = new Uint8Array(8);
target.setFromBase64("aGVsbG8K"); // { read: 8, written: 6 }
Comprueba que existe antes de depender de ella: typeof Uint8Array.prototype.toBase64 === "function". La guía de Uint8Array.toBase64 repasa todas las opciones.
Base64 URL-safe con y sin relleno
Con la API nueva, pasa opciones:
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 "<<???>>"
Con btoa/atob, traduce tú el alfabeto:
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")); // "<<???>>"
Esto es lo que usan los JWT; consulta Base64 frente a Base64URL o pega un token en el decodificador JWT.
Archivos, Blobs y datos grandes
Para leer un archivo elegido por el usuario, FileReader.readAsDataURL devuelve un data URI. Quita todo lo que haya hasta la coma para quedarte con el Base64:
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);
Con la API nueva es más corto: new Uint8Array(await file.arrayBuffer()).toBase64().
Para convertir el Base64 de nuevo en un archivo descargable, decodifícalo a bytes y envuélvelos en un Blob:
const bytes = Uint8Array.fromBase64(b64);
const url = URL.createObjectURL(new Blob([bytes], { type: "application/pdf" }));
Con búferes grandes, nunca los expandas en String.fromCharCode(...bytes). Con 500 000 bytes, Chromium lanza RangeError: Maximum call stack size exceeded. Construye la cadena binaria por tramos:
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
La salida ocupa aproximadamente un tercio más que la entrada; la guía del aumento de tamaño tiene la fórmula exacta.
Validación y mensajes de error
atob ignora los espacios en blanco ASCII y tolera la falta de relleno, así que atob(" aG k= ") y atob("aGk") devuelven "hi". Rechaza los caracteres fuera del alfabeto estándar:
atob("PDw_Pz8-Pg");
// InvalidCharacterError: Failed to execute 'atob' on 'Window':
// The string to be decoded is not correctly encoded.
fromBase64 lanza SyntaxError y se puede hacer más estricto con 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.
El valor por defecto, "loose", acepta que falte el relleno. "strict" exige un relleno correcto y bits de relleno a cero; "stop-before-partial" deja sin procesar un último bloque incompleto, pensado para decodificadores en flujo. El texto de los mensajes cambia de un navegador a otro, pero los nombres de los errores son estándar. Para una comprobación visual, el validador Base64 señala el problema exacto, y errores habituales de Base64 enumera las causas.
Preguntas frecuentes
¿Por qué btoa lanza InvalidCharacterError?
btoa solo acepta caracteres del rango Latin-1 (puntos de código 0–255). Cualquier emoji o carácter cirílico o CJK hace que falle. Convierte antes la cadena en bytes UTF-8 con TextEncoder, o usa Uint8Array.prototype.toBase64().
¿Puedo usar Uint8Array.toBase64 en producción?
Está disponible en las versiones actuales de Chrome, Edge, Firefox y Safari, pero no en navegadores antiguos ni en Node.js 24. Detecta la función con typeof Uint8Array.prototype.toBase64 === "function" y recurre a la función auxiliar con btoa si no existe. Detalles en Uint8Array.toBase64 explicado.
¿Por qué atob devuelve caracteres raros como é en lugar de é?
atob devuelve un carácter por byte, así que una secuencia UTF-8 de dos bytes se convierte en dos caracteres Latin-1. Pasa el resultado por Uint8Array.from(bin, c => c.charCodeAt(0)) y TextDecoder para obtener el texto correcto.
¿Cómo decodifico Base64URL con atob?
Sustituye - por + y _ por /, y después rellena con = hasta un múltiplo de 4 caracteres. Con la API nueva, llama a Uint8Array.fromBase64(s, { alphabet: "base64url" }) y te ahorras los dos pasos.