Three Base64 Encoders in the JavaScript Ecosystem
JavaScript has three built-in ways to produce Base64, each for a different context:
- btoa() — browser only, Latin-1 only, throws on Unicode
- Buffer.from(x).toString("base64") — Node.js only, UTF-8 native
- TextEncoder + btoa() — browser, UTF-8 safe, the modern correct pattern
Picking the right one depends on where your code runs. Server-side (Node, Deno, Bun): use Buffer. Client-side (browser, service worker, Electron renderer): use theTextEncoder + btoa() combo. Universal libraries (isomorphic code): feature-detect at load time and choose accordingly.
Browser: The UTF-8-Safe Pattern
This is the pattern every modern JavaScript codebase should use in the browser:
function encodeBase64(text) {
const utf8Bytes = new TextEncoder().encode(text);
const binString = String.fromCharCode(...utf8Bytes);
return btoa(binString);
}
// Works on ASCII, emojis, CJK, everything:
encodeBase64('Hello'); // "SGVsbG8="
encodeBase64('Hello 👋 世界'); // "SGVsbG8g8J+RiyDkuJbnlYw="
encodeBase64('café résumé'); // "Y2Fmw6kgcsOpc3Vtw6k="
// Decode round trip:
function decodeBase64(b64) {
const binString = atob(b64);
const bytes = Uint8Array.from(binString, c => c.charCodeAt(0));
return new TextDecoder().decode(bytes);
}Why not just call btoa(text) directly? Because if text ever contains a character above code point 255 (which includes every emoji, every non-Latin script, and even some accented Latin characters like ő or ż on some pages), btoa() throwsInvalidCharacterError. The TextEncoder step converts the string to UTF-8 bytes so btoa() sees only single-byte characters.
Node.js: Buffer Is the Canonical Approach
// Encode a string — UTF-8 is the default
const encoded = Buffer.from('Hello 世界').toString('base64');
console.log(encoded); // "SGVsbG8g5LiW55WM"
// Encode a file — read as buffer, output as Base64
import { readFileSync } from 'node:fs';
const fileB64 = readFileSync('./image.png').toString('base64');
// Encode raw bytes
const bytes = new Uint8Array([72, 101, 108, 108, 111]);
const b64 = Buffer.from(bytes).toString('base64'); // "SGVsbG8="
// URL-safe variant (Node 16+)
const urlSafe = Buffer.from('data with + and /').toString('base64url');
// "ZGF0YSB3aXRoICsgYW5kIC8" (no padding, - / _ replaced)Encoding a File Upload to Base64 in the Browser
When the user picks a file with <input type="file">, useFileReader to read the file and get a Base64 data URL directly:
function fileToBase64DataUrl(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result); // "data:image/png;base64,iVBOR..."
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(file);
});
}
// If you want just the Base64 payload (without the data:...;base64, prefix):
async function fileToBase64Only(file) {
const dataUrl = await fileToBase64DataUrl(file);
return dataUrl.split(',')[1];
}
// Usage:
document.querySelector('input[type=file]').addEventListener('change', async (e) => {
const b64 = await fileToBase64Only(e.target.files[0]);
console.log(b64); // send to API, embed in JSON, etc.
});Building URL-Safe Base64 Manually (for JWT / URL query strings)
function base64UrlEncode(text) {
const utf8Bytes = new TextEncoder().encode(text);
const binString = String.fromCharCode(...utf8Bytes);
return btoa(binString)
.replace(/\+/g, '-') // + becomes -
.replace(/\//g, '_') // / becomes _
.replace(/=+$/, ''); // strip trailing = padding
}
base64UrlEncode('Hello 世界'); // "SGVsbG8g5LiW55WM" (no + or /, no trailing =)
// Reverse — Base64URL decode requires putting the padding back:
function base64UrlDecode(b64url) {
const b64 = b64url.replace(/-/g, '+').replace(/_/g, '/');
const padded = b64 + '='.repeat((4 - b64.length % 4) % 4);
return new TextDecoder().decode(
Uint8Array.from(atob(padded), c => c.charCodeAt(0))
);
}Encoding for HTTP Basic Auth in a Fetch Call
async function callProtectedApi(user, pass) {
const token = btoa(`${user}:${pass}`); // ASCII credentials — btoa is safe
const res = await fetch('https://api.example.com/protected', {
headers: { Authorization: `Basic ${token}` }
});
return res.json();
}
// If user/pass contain Unicode, use the UTF-8 pattern:
function encodeBasicAuth(user, pass) {
const creds = `${user}:${pass}`;
return btoa(String.fromCharCode(...new TextEncoder().encode(creds)));
}Common Mistakes and How to Fix Them
- Calling btoa() on a Unicode string — throws InvalidCharacterError. Fix: wrap the string with the TextEncoder pattern.
- Encoding a Uint8Array with btoa() directly — you get the wrong output because Uint8Array coerces to a comma-separated string. Fix:
btoa(String.fromCharCode(...bytes)). - Assuming Node's Buffer works in the browser — Buffer is not built-in in the browser. Use TextEncoder + btoa instead, or import a polyfill like
buffer. - Forgetting to strip the data URL prefix —
FileReader.readAsDataURLreturnsdata:image/png;base64,iVBOR.... If your API wants just the Base64, split on the comma and take the second half. - Mixing standard and URL-safe Base64 — a decoder built for standard Base64 will fail on URL-safe input (unrecognised - and _). Always know which variant each side is using and convert between them explicitly.
Key Facts
- Browser function:
- btoa() — Latin-1 only, needs TextEncoder for UTF-8
- Node function:
- Buffer.from(x).toString('base64') — UTF-8 native
- URL-safe (Node 16+):
- Buffer.from(x).toString('base64url')
- File encoding:
- FileReader.readAsDataURL() in browser, readFileSync(..., 'base64') in Node
- Reverse:
- atob() in browser, Buffer.from(x, 'base64').toString('utf8') in Node
Related Base64 Tools
- Base64 Encode Online — general-purpose encoder
- Base64 Encode String — deep dive on string encoding
- Text to Base64 Converter — plain-text conversion
- Base64 Encode in Python — Python equivalent patterns
- Base64 Decode — decode back to text
- JWT Debugger — inspect JWT tokens (Base64URL segments)