Subir un archivo
  1. base64hub
  2. Base64 en código
  3. JavaScript
Código

Base64 en JavaScript

Los navegadores tienen btoa y atob, que solo manejan Latin-1, y los más recientes Uint8Array.toBase64() / fromBase64(), que trabajan con bytes como es debido. Las salidas de esta página son de Chromium headless.

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.

Sigue leyendo