Herramientas

Imagen Video Audio Texto Vector Desarrollador General

⚡ 20 créditos gratuitos restantes hoy

Regístrate gratis

Cómo convertir texto a Base64 en JavaScript (y decodificarlo)

La línea de código que funciona, la que falla y por qué falla: además del viaje de ida y vuelta seguro para UTF-8, los métodos nativos de 2025 y las diferencias en Node.js.

Ilustración suave en 3D de formas de documentos azules superpuestos que se transforman en una cuadrícula de cubos azules codificados que se disuelven en…

Para codificar en base64 una cadena en JavaScript, llama a btoa('Hello, world!'), que devuelve 'SGVsbG8sIHdvcmxkIQ=='. Esta línea única solo funciona para texto ASCII y Latin-1: en cuanto pasas un emoji, un acento o un carácter chino, lanza InvalidCharacterError. Esta guía muestra por qué ocurre esto y el viaje de ida y vuelta seguro para UTF-8 que lo soluciona, incluidos los nuevos métodos nativos de 2025.

Base64 en una línea: btoa() y atob()

La respuesta más rápida: codifica con btoa('Hello, world!'), decodifica con atob('SGVsbG8sIHdvcmxkIQ=='). Ambos verificados: btoa('a') devuelve 'YQ==', y atob('SA==') devuelve 'S'.

Lo que btoa() hace realmente es tratar tu cadena como una "cadena binaria", una cadena donde cada carácter representa exactamente un byte de datos binarios, y luego codifica esos bytes en Base64. El caso de uso de MDN son datos que causan problemas de transmisión, como los caracteres de control ASCII (0–31), que quieres colar a través de un canal solo de texto.

Esta línea única es suficiente cuando tu texto es ASCII: el punto de código de cada carácter debe ser inferior a 256, y según MDN, la correspondencia byte-punto de código solo se mantiene de forma fiable hasta 0x7F. El rango 0x80–0xFF funciona técnicamente, pero depende del comportamiento de codificación Latin-1 que no es portátil entre entornos. Una buena regla: cualquier cosa no ASCII necesita la ruta UTF-8 que se indica a continuación.

Por qué btoa() lanza InvalidCharacterError con emojis, acentos y CJK

Base64 es una codificación binaria, no una codificación de texto. El glosario de MDN califica el hábito de pasar texto o HTML arbitrario a btoa() como un malentendido común: btoa/atob son anteriores a los arrays tipados, por lo que usan cadenas para representar bytes. El problema empieza cuando tu cadena no tiene un byte por carácter.

Las cadenas de JavaScript son UTF-16, por lo que una unidad de código siempre ocupa dos bytes, y los caracteres fuera del BMP (como los emojis) ocupan cuatro bytes como par sustituto; UTF-8 usa de uno a cuatro bytes por punto de código. btoa() y atob() solo funcionan con caracteres de un solo byte (rango Latin-1). Intenta codificar algo más ancho y obtendrás este error exacto:

DOMException: Failed to execute 'btoa' on 'Window':
The string to be encoded contains characters outside of the Latin1 range

El caso que falla es trivial de reproducir: btoa('✓') lanza un error, y también lo hace btoa('a Ā 𐀀 文 🦄'). Los emojis, las letras acentuadas fuera de Latin-1 y los caracteres CJK están todos por encima de 0xFF, por lo que ninguno se ajusta a la suposición de un byte por carácter. '✓' es U+2713, muy por encima de 0xFF, por lo que btoa() se niega en lugar de corromper tus datos silenciosamente. Esta negativa es probablemente la mejor característica de la API: falla ruidosamente en lugar de producir mojibake que descubrirías semanas después.

Otra cosa que vale la pena saber antes de solucionar esto: Base64 hace que los datos sean más grandes, aproximadamente un 33 % más en comparación con los bytes crudos. Está bien para cadenas cortas, tokens y URI de datos, pero es una razón real para no codificar en Base64 cargas grandes: codifica en el límite que necesitas, no en todas partes.

Un listón azul suave en 3D que entra desde la izquierda y se divide en dos caminos: una hebra forma grupos ordenados y agrupados de…
Un flujo de datos, dos huellas de bytes: clústeres de varios bytes frente a bytes individuales.

El caso extremo del sustituto solitario (y String.isWellFormed())

La mayoría de los artículos pasan por alto esto, pero duele: una cadena UTF-16 que contiene un sustituto solitario (la mitad de un par de emojis, normalmente por un error de segmentación) está "malformada". Según la guía de web.dev, TextDecoder sustituye silenciosamente U+FFFD por defecto. Antes de codificar, verifica que esté bien formada con String.prototype.isWellFormed(), o captura el URIError que encodeURIComponent() lanza con sustitutos solitarios, y decide explícitamente: rechazar los datos, habilitar la sustitución deliberadamente o lanzar un error para un análisis posterior.

La forma correcta: TextEncoder → bytes UTF-8 → Base64

La solución correcta es convertir tu cadena a bytes UTF-8 con TextEncoder primero, y luego codificar en Base64 los bytes en lugar de la cadena. new TextEncoder().encode(str) convierte cualquier cadena de JavaScript en una Uint8Array de bytes UTF-8 (TextEncoder solo habla UTF-8). De vuelta, new TextDecoder() usa UTF-8 por defecto y lo invierte exactamente. Fíjate en lo que atob() te da aquí: una cadena binaria, un carácter por byte, no texto UTF-8. Por eso el lado de decodificación necesita TextDecoder para convertir los bytes de nuevo a texto legible.

Auxiliares bytesToBase64() y base64ToBytes() (código para copiar y pegar)

Aquí tienes el viaje de ida y vuelta canónico de MDN, que se ejecuta en todos los navegadores que soportan TextEncoder (IE excluido):

function bytesToBase64(bytes) {
  const binString = Array.from(bytes, (b) => String.fromCodePoint(b)).join('');
  return btoa(binString);
}

function base64ToBytes(base64) {
  const binString = atob(base64);
  return Uint8Array.from(binString, (m) => m.codePointAt(0));
}

Verifica con un viaje de ida y vuelta completo

Codificar una cadena se reduce a una línea:

const encoded = bytesToBase64(new TextEncoder().encode('a Ā 𐀀 文 🦄'));
// 'YSDEgCDwkICAIOaWhyDwn6aE'

Decodificar lo revierte:

const decoded = new TextDecoder().decode(base64ToBytes(encoded));
// 'a Ā 𐀀 文 🦄'

El viaje de ida y vuelta verificado de web.dev confirma el mismo patrón: 'hello⛳❤️🧀' codifica a 'aGVsbG/im7PinaTvuI/wn6eA' y decodifica exactamente de vuelta, incluidos los emojis.

Omite el truco unescape(encodeURIComponent())

Encontrarás btoa(unescape(encodeURIComponent(str))) copiado por todas partes. Funciona, pero unescape() es legado del Anexo B, obsoleto desde ECMAScript 3. El código nuevo debería usar TextEncoder en su lugar: es explícito, estándar y no depende de un global obsoleto.

La forma de 2025: Uint8Array.toBase64() y Uint8Array.fromBase64()

Desde septiembre de 2025, JavaScript tiene métodos nativos de Base64 en Uint8Array mismos, por lo que los auxiliares de cadena de bytes anteriores ya no son la única opción. La propuesta Base64 de TC39 alcanzó la Etapa 4 y forma parte del lenguaje; la justificación de TC39 es que base64 no es una codificación de texto, por lo que TextEncoder/TextDecoder son conceptualmente la herramienta incorrecta para ello.

Según MDN, estas funciones están en la categoría «Newly available» (recién disponibles) en todos los navegadores modernos. Comprueba los entornos de ejecución que soportas antes de lanzar: si necesitas compatibilidad con navegadores antiguos o versiones viejas de Node, mantén el respaldo con TextEncoder + btoa. MDN recomienda estos métodos sobre btoa()/atob() cuando ya tienes un Uint8Array, porque no hace falta crear una cadena cruda de bytes.

Codificación: bytes.toBase64()

const b64 = new TextEncoder().encode('Hello, world!').toBase64();
// 'SGVsbG8sIHdvcmxkIQ=='

Una conversión a UTF-8, una llamada al método y listo.

Decodificación: Uint8Array.fromBase64()

const text = new TextDecoder().decode(Uint8Array.fromBase64(b64));
// 'Hello, world!'

MDN indica que fromBase64() es preferible a atob() porque devuelve un array de bytes real, salvo que los datos decodificados vayan a tratarse como texto ASCII. Solo acepta el alfabeto base64 (A–Z, a–z, 0–9, más +/ o -_), el relleno = opcional e ignora los espacios en blanco ASCII; lanza SyntaxError si encuentra caracteres fuera del alfabeto.

Opciones: alphabet, omitPadding, lastChunkHandling

El conjunto de opciones de la especificación te da control preciso:

Opción Valores Qué hace
alphabet 'base64' (predeterminado), 'base64url' +// frente a -/_
omitPadding booleano Elimina el relleno = final: new Uint8Array([72]).toBase64() → 'SA==', con omitPadding → 'SA'
lastChunkHandling 'loose' (predeterminado), 'strict', 'stop-before-partial' Tolerancia del decodificador para el último fragmento

'loose' tolera la falta de relleno y espacios en blanco. 'strict' exige fragmentos exactos de 4 caracteres con bits desbordados en cero, lo cual es lo adecuado al validar entradas no confiables. 'stop-before-partial' está pensado para streaming. El modo suelto predeterminado también tolera bits desbordados distintos de cero, algo que el modo estricto rechaza.

Base64 frente a base64url: ¿qué alfabeto necesitas?

El base64 estándar usa + y / más el relleno =, y es válido para data: URLs porque no tienen segmentos de ruta ni cadenas de consulta. base64url (RFC 4648 §5) sustituye por - y _ y suele omitir el relleno; es obligatorio cuando la salida viaja dentro de una URL: los JWT usan base64url para sus tres segmentos, por ejemplo.

Cuándo usar base64url

Usa base64url cuando la cadena codificada forme parte de la ruta o parámetros de consulta de una URL, donde + y / requerirían escapado (y el relleno = ensucia las cadenas de consulta). Emplea base64 estándar para data URIs, para incrustar bytes en cargas JSON o para cualquier cosa que no sea una URL.

Cómo producir base64url

Tres formas, según el entorno:

  • Navegadores modernos: bytes.toBase64({ alphabet: 'base64url' })
  • Node 16+: Buffer.from(str).toString('base64url')
  • Postprocesamiento: toma la salida estándar y reemplaza + → -, / → _, luego quita el relleno =

Un detalle sutil que conviene saber: RFC 4648 exige que los codificadores pongan en cero los bits de desborde que no son datos en el último fragmento, y los decodificadores estrictos pueden lanzar error si no están en cero. El glosario de MDN pone el ejemplo de 'VR==', que es el mismo byte que 'VQ==' pero con bits de relleno sucios. Genera base64 con un codificador correcto y esto nunca ocurre; si lo haces a mano, podría pasar.

Codificar un archivo o imagen a Base64 (data URLs)

Para archivos, la vía del navegador es FileReader con readAsDataURL(), que produce una data URL completa; luego quitas el prefijo para obtener el base64 crudo. Para imágenes ya renderizadas en un canvas, toDataURL('image/png') lo hace en una sola llamada.

FileReader.readAsDataURL() → data URL → base64 crudo

function fileToDataURL(file) {
  return new Promise((resolve) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.readAsDataURL(file);
  });
}

const dataUrl = await fileToDataURL(fileInput.files[0]);
const rawBase64 = dataUrl.replace(/^data:\w+\/\w+;base64,/, '');

canvas.toDataURL() para imágenes

Si has dibujado una imagen en un canvas (redimensionar, recortar, etc.), salta FileReader por completo:

const dataUrl = canvas.toDataURL('image/png');

La misma eliminación de prefijo aplica si necesitas solo la cadena base64 pura.

Data URL de vuelta a bytes

En sentido inverso, se trata de un fetch más un array buffer. El ejemplo de MDN: una data URL construida desde new Uint8Array([0,1,2]) es 'data:application/octet-stream;base64,AAEC', y puedes recuperar los bytes así:

const res = await fetch(dataUrl);
const bytes = new Uint8Array(await res.arrayBuffer());
// [0, 1, 2]

Y el recordatorio por el que existe todo este artículo: no llames a btoa() sobre texto o HTML arbitrarios para construir data URLs. Es un codificador byte a byte, y entradas no Latin-1 lanzarán error.

Node.js: Buffer.from(str, 'base64') — y su única diferencia peligrosa

Node tiene sus propios patrones que evitan por completo el baile de TextEncoder: Buffer.from('Hello, world! 🌍').toString('base64') maneja Unicode automáticamente, porque Buffer trabaja sobre bytes desde el inicio. Decodifica con Buffer.from(encoded, 'base64').toString('utf8').

Otras formas útiles: Buffer.from(str).toString('base64url') (Node 16+), y archivos completos mediante fs.readFileSync('image.png').toString('base64').

const encoded = Buffer.from('Hello, world! 🌍').toString('base64');
const decoded = Buffer.from(encoded, 'base64').toString('utf8');

Node no valida tu base64 (fallos silenciosos)

Esto sorprende a mucha gente: el decodificador base64 de Node no valida la entrada. Buffer.from('!!!!', 'base64').toString() devuelve '' sin error, y Buffer.from('aGVsbG8!!!', 'base64') descarta silenciosamente la basura y retorna 'hello'. Compara eso con atob('aGVsbG8!!!'), que lanza InvalidCharacterError. Si el base64 proviene de usuarios, una API o un tercero, valídalo antes de decodificarlo en Node, o usa Uint8Array.fromBase64() con lastChunkHandling: 'strict' donde los entornos lo permitan.

¿Están btoa/atob obsoletas? Navegadores vs Node

Esto genera conflicto real, así que aquí va la resolución honesta. En navegadores, MDN marca btoa() y atob() como ampliamente disponibles desde julio de 2015, no obsoletas. En Node, las definiciones de tipos las marcan como @deprecated con el comentario de que «no deberían usarse en código nuevo», recomendando Buffer.from(str, 'base64') en su lugar.

Buena regla: es una API heredada, no eliminada. Válida en código de navegador; prefiere Buffer en código específico de Node. Por consenso comunitario (no garantizado por la especificación), btoa/atob son también la opción práctica multiplataforma si necesitas un único camino de código que corra sin cambios en navegadores, Deno y Node sin dependencias.

Qué usar realmente

Si te quedas con una sola recomendación de este artículo, son las tres líneas siguientes. Mantén btoa/atob para ASCII puro y bases de código antiguas, y usa la utilidad bytesToBase64 como respaldo en el navegador hasta que toBase64() cubra a toda tu audiencia.

// Modern browsers (2025+)
const enc = new TextEncoder().encode(str).toBase64();
const dec = new TextDecoder().decode(Uint8Array.fromBase64(b64));

// Browser fallback (MDN helper)
const encFallback = bytesToBase64(new TextEncoder().encode(str));

// Node (validate input first if it isn't yours)
const encNode = Buffer.from(str).toString('base64');

Antes de lanzar, verifica tu salida con un viaje de ida y vuelta rápido en una herramienta de navegador. Pega 'Hello, world!' en Codificador / Decodificador Base64 gratuito de Tool Cloud, confirma que obtienes 'SGVsbG8sIHdvcmxkIQ==', luego decodifícalo de nuevo y comprueba que es idéntico. También hay un convertidor de Texto a Base64 y un convertidor de Base64 a Texto; los archivos se borran automáticamente y no hace falta instalar nada.

Preguntas frecuentes

¿Por qué btoa() falla con emojis pero funciona con texto en inglés?

btoa() trata cada carácter como un solo byte y requiere que cada punto de código sea inferior a 256 (de forma fiable solo hasta 0x7F). Los emojis, los caracteres con acento y el texto CJK necesitan múltiples bytes, por lo que provocan un DOMException: InvalidCharacterError. Convierte primero a bytes UTF-8 con TextEncoder y luego codifica esos bytes.

¿Está obsoleto btoa()?

No en los navegadores: MDN marca btoa() y atob() como Baseline, ampliamente disponibles desde julio de 2015. Sin embargo, las definiciones de tipos de Node los marcan como obsoletos en favor de Buffer.from(str, 'base64') en código específico de Node. Son una API heredada, no una eliminada.

¿Cómo codifico en Base64 texto Unicode (acentos, chino, emojis)?

Usa new TextEncoder().encode(str) para obtener los bytes UTF-8 y luego codifica los bytes (btoa() mediante un auxiliar de cadena de bytes, o Uint8Array.toBase64() en navegadores modernos). Para decodificar, usa Uint8Array.fromBase64() o atob() para recuperar los bytes y luego new TextDecoder().decode(bytes).

¿Cuál es la diferencia entre base64 y base64url?

base64url (RFC 4648 §5) utiliza '-' y '_' en lugar de '+' y '/', y suele omitir el relleno '='. Usa base64url cuando la salida va a parar a URLs o JWTs; el base64 estándar está bien para data: URLs, que no tienen segmentos de ruta ni cadenas de consulta.

¿Por qué Node.js no lanza un error con base64 inválido?

Buffer.from(str, 'base64') de Node no valida la entrada: ignora silenciosamente los caracteres inválidos, por lo que Buffer.from('aGVsbG8!!!', 'base64').toString() devuelve 'hello' sin error. En los navegadores, atob() lanza InvalidCharacterError con la misma entrada. Valida el base64 no confiable antes de decodificarlo en Node.