Base64 Encoding in JavaScript: btoa, atob, and Beyond
JavaScript provides built-in functions for Base64 encoding and decoding, but they come with important caveats — particularly around Unicode handling. This guide covers the browser APIs, the Node.js approach, techniques for handling binary data and UTF-8 strings, URL-safe encoding, and common pitfalls that catch developers off guard.
The Built-in Functions: btoa() and atob()
Every modern browser provides two global functions for Base64:
btoa(string)— Encodes a string to Base64 ("binary to ASCII")atob(base64)— Decodes a Base64 string back to a binary string ("ASCII to binary")
const encoded = btoa('Hello World');
console.log(encoded); // "SGVsbG8gV29ybGQ="
const decoded = atob('SGVsbG8gV29ybGQ=');
console.log(decoded); // "Hello World"For plain ASCII strings, these functions work perfectly. The trouble starts when you move beyond ASCII.
The Unicode Problem
btoa() only accepts strings where every character has a code point in the range 0-255 (the Latin-1 / ISO-8859-1 range). If you pass a string containing characters outside this range — which includes most non-English text, emoji, and many symbols — it throws an error:
btoa('Hello 🌍');
// Uncaught DOMException: Failed to execute 'btoa' on 'Window':
// The string to be encoded contains characters outside of the
// Latin1 range.This is a fundamental limitation. Despite the name "binary to ASCII," btoa()does not actually handle arbitrary binary data or Unicode text. You need a workaround.
The TextEncoder/TextDecoder Approach for UTF-8
The modern solution for encoding Unicode strings as Base64 in the browser is to use TextEncoder to first convert the string into UTF-8 bytes, then encode those bytes as Base64. For decoding, you reverse the process with TextDecoder.
// Encode a Unicode string to Base64
function encodeBase64(str) {
const encoder = new TextEncoder();
const bytes = encoder.encode(str);
const binString = Array.from(bytes, (byte) =>
String.fromCodePoint(byte)
).join('');
return btoa(binString);
}
// Decode Base64 back to a Unicode string
function decodeBase64(base64) {
const binString = atob(base64);
const bytes = Uint8Array.from(binString, (char) =>
char.codePointAt(0)
);
const decoder = new TextDecoder();
return decoder.decode(bytes);
}
// Now Unicode works correctly
const encoded = encodeBase64('Hello 🌍');
console.log(encoded); // "SGVsbG8g8J+MjQ=="
console.log(decodeBase64(encoded)); // "Hello 🌍"This two-step approach — string to UTF-8 bytes, then bytes to Base64 — is the reliable pattern for all Unicode text. TextEncoder always produces UTF-8, which is the encoding you almost always want.
Node.js: Buffer.from()
Node.js does not have btoa() and atob() in older versions (they were added in Node 16). The idiomatic Node.js approach uses Buffer:
// Encode string to Base64
const encoded = Buffer.from('Hello World', 'utf-8').toString('base64');
console.log(encoded); // "SGVsbG8gV29ybGQ="
// Decode Base64 to string
const decoded = Buffer.from('SGVsbG8gV29ybGQ=', 'base64').toString('utf-8');
console.log(decoded); // "Hello World"
// Unicode works natively with Buffer
const emoji = Buffer.from('Hello 🌍', 'utf-8').toString('base64');
console.log(emoji); // "SGVsbG8g8J+MjQ=="
// URL-safe Base64 (Node 16+)
const urlSafe = Buffer.from('Hello World').toString('base64url');
console.log(urlSafe); // "SGVsbG8gV29ybGQ"Buffer.from() handles UTF-8 natively, so you do not need the TextEncoder workaround. It also supports the base64url encoding directly (added in Node 16), which produces URL-safe Base64 without padding.
Handling Binary Data: Uint8Array to Base64
When you are working with actual binary data in the browser — such as file contents, image data, or cryptographic output — you are typically dealing with ArrayBuffer or Uint8Array. Converting these to Base64 requires going through a binary string:
// Uint8Array → Base64
function uint8ArrayToBase64(bytes) {
const binString = Array.from(bytes, (byte) =>
String.fromCodePoint(byte)
).join('');
return btoa(binString);
}
// Base64 → Uint8Array
function base64ToUint8Array(base64) {
const binString = atob(base64);
return Uint8Array.from(binString, (char) =>
char.codePointAt(0)
);
}
// Example: encode binary data
const bytes = new Uint8Array([72, 101, 108, 108, 111]);
console.log(uint8ArrayToBase64(bytes)); // "SGVsbG8="URL-Safe Base64 in JavaScript
The browser does not provide a built-in URL-safe Base64 function. You need to perform the character substitutions yourself after encoding, or before decoding:
// Encode to URL-safe Base64 (no padding)
function toBase64Url(str) {
return btoa(str)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
// Decode from URL-safe Base64
function fromBase64Url(base64url) {
// Replace URL-safe chars with standard chars
let base64 = base64url
.replace(/-/g, '+')
.replace(/_/g, '/');
// Re-add padding
while (base64.length % 4 !== 0) {
base64 += '=';
}
return atob(base64);
}
console.log(toBase64Url('subjects?')); // "c3ViamVjdHM_"
console.log(fromBase64Url('c3ViamVjdHM_')); // "subjects?"For the full UTF-8-safe version, combine this with the TextEncoder approach shown earlier: first encode the string to UTF-8 bytes, convert to a binary string, apply btoa(), then do the URL-safe replacements.
Practical Examples
Encoding File Uploads as Base64
A common use case is reading a file selected by the user and converting it to a Base64 string for upload via a JSON API:
function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
// reader.result is a data URI like "data:image/png;base64,iVBOR..."
// Extract just the Base64 portion
const base64 = reader.result.split(',')[1];
resolve(base64);
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
// Usage with a file input
const input = document.querySelector('input[type="file"]');
input.addEventListener('change', async (e) => {
const file = e.target.files[0];
const base64 = await fileToBase64(file);
// Send to API
fetch('/api/upload', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: file.name, data: base64 }),
});
});Creating and Reading Data URIs
Data URIs embed file contents directly in URLs. You can create them from Base64 strings and also extract the Base64 data from existing data URIs:
// Create a data URI from a string
const text = 'Hello, World!';
const base64 = btoa(text);
const dataUri = `data:text/plain;base64,${base64}`;
// "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=="
// Extract Base64 from a data URI
function extractBase64(dataUri) {
const [header, data] = dataUri.split(',');
const isBase64 = header.includes(';base64');
return isBase64 ? atob(data) : decodeURIComponent(data);
}Decoding JWT Payloads
JWTs use URL-safe Base64 (without padding) for their header and payload. You can decode the payload to inspect claims without verifying the signature:
function decodeJwtPayload(token) {
const parts = token.split('.');
if (parts.length !== 3) throw new Error('Invalid JWT');
// Decode the payload (second segment)
let payload = parts[1]
.replace(/-/g, '+')
.replace(/_/g, '/');
while (payload.length % 4 !== 0) {
payload += '=';
}
const json = atob(payload);
return JSON.parse(json);
}
const jwt = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4ifQ.xxx';
console.log(decodeJwtPayload(jwt));
// { sub: "1234567890", name: "John" }Common Pitfalls
1. Assuming btoa() Handles Unicode
As discussed above, btoa() throws on any character above code point 255. Always use the TextEncoder approach for non-ASCII text.
2. Forgetting to Restore Padding
URL-safe Base64 strings often have padding stripped. If you pass an unpadded string to atob(), it may fail or produce incorrect output in some environments. Always re-add = characters to make the length a multiple of 4 before decoding.
3. Encoding Strings vs. Bytes
A common confusion: btoa('hello') and Buffer.from('hello').toString('base64') produce the same result for ASCII text, but they operate on different abstractions. btoa() works on a "binary string" (each character is a byte), while Buffer works on actual bytes. This distinction matters when interoperating between browser and server code.
4. Double Encoding
Watch out for accidentally Base64-encoding data that is already Base64-encoded. This produces a valid but meaningless string that is twice the expected size. If your decoded output still looks like Base64, you have probably double-encoded somewhere.
5. Performance with Large Files
Base64 encoding a large file (several MB) in the browser using the string concatenation methods above can be slow and memory-intensive. For large files, consider using FileReader.readAsDataURL() which handles the encoding natively, or process the data in chunks using streaming APIs.
Related Guides
- What Is Base64? — Overview of Base64 encoding and why it exists
- URL-Safe Base64 — The variant designed for URLs, JWTs, and filenames
- Data Encoding Overview — Comparing Base64, hex, URL encoding, and other schemes