atob() Is Older Than Modern JavaScript — And That Matters
atob()was added to browsers back when JavaScript strings could safely represent any character in the Latin-1 range (0-255). It was designed to decode Base64 to a "binary string" — one JavaScript character per byte. When JavaScript adopted UTF-16 for internal string representation and the web adopted UTF-8 for text transport, atob()didn't change. It still returns raw byte codes as characters, leaving UTF-8 decoding as your job.
Everyone forgets this the first time they hit it. Your JSON payload has a Chinese name, you atob()the Base64, and get back gibberish. The bug isn't in atob — it's in expecting atob to know about UTF-8. Modern code always pairs atob withTextDecoder.
Correct Base64 Decode in Browser JavaScript
// ✅ Recommended pattern — UTF-8 safe
function decodeBase64(str) {
const binaryString = atob(str);
const bytes = Uint8Array.from(binaryString, c => c.charCodeAt(0));
return new TextDecoder('utf-8').decode(bytes);
}
decodeBase64('SGVsbG8gV29ybGQ=');
// → "Hello World"
decodeBase64('SGVsbG8g5LiW55WMIPCfkYs=');
// → "Hello 世界 👋" (works with emoji and CJK)
// ❌ Broken — DO NOT use this pattern anymore
// The deprecated "escape + atob" hack:
decodeURIComponent(escape(atob(str)));
// escape() is deprecated. Do not use in new code.Correct Base64 Decode in Node.js
// Node.js Buffer handles UTF-8 natively
const decoded = Buffer.from('SGVsbG8gV29ybGQ=', 'base64').toString('utf-8');
console.log(decoded); // "Hello World"
// URL-safe variant
const decoded2 = Buffer.from('SGVsbG8_', 'base64url').toString('utf-8');
// base64url handles - / _ and missing padding automatically (Node 16+)
// Buffer works with any binary data — great for files
const fileBytes = Buffer.from(base64ImageString, 'base64');
require('fs').writeFileSync('./image.png', fileBytes);Decoding a JWT Payload in JavaScript
// JWT format: header.payload.signature (all URL-safe Base64, no padding)
function decodeJwtPayload(token) {
const [, payload] = token.split('.');
// Normalize URL-safe → standard, add padding
const b64 = payload.replace(/-/g, '+').replace(/_/g, '/');
const padded = b64 + '='.repeat((4 - b64.length % 4) % 4);
// UTF-8 decode
const json = new TextDecoder().decode(
Uint8Array.from(atob(padded), c => c.charCodeAt(0))
);
return JSON.parse(json);
}
const claims = decodeJwtPayload(myJwt);
console.log(claims.exp, claims.sub, claims.name);Decoding a Base64 Image (Data URL) in the Browser
function base64ToImageBlob(dataUrl) {
// Split "data:image/png;base64,iVBORw0KG..."
const [prefix, b64] = dataUrl.split(',');
const mime = prefix.match(/data:([^;]+)/)[1];
const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
return new Blob([bytes], { type: mime });
}
// Display it
const blob = base64ToImageBlob('data:image/png;base64,iVBOR...');
const url = URL.createObjectURL(blob);
document.getElementById('preview').src = url;
// Remember to revoke when done:
// URL.revokeObjectURL(url);The atob() vs Buffer Compatibility Table
| Aspect | Browser atob() | Node.js Buffer |
|---|---|---|
| UTF-8 handling | Manual (needs TextDecoder) | Native (.toString("utf-8")) |
| URL-safe variant | Manual char replacement | Built-in ("base64url") |
| Missing padding | Manual fix required | Auto-handled (base64url) |
| Large payloads | OK up to ~1 MB in one call | Handles multi-GB streams |
| Availability | Every browser + Node 16+ | Node only |
Common Errors and Fixes
- DOMException: Failed to execute 'atob': The string to be decoded is not correctly encoded— non-Base64 characters in the input. Strip whitespace, quotes, and any "data:...;base64," prefix first.
- Garbled UTF-8 output — using atob() without TextDecoder. See the recommended pattern above.
- "String contains an invalid character"— input length isn't a multiple of 4. Add padding:
str + '='.repeat((4 - str.length % 4) % 4). - Emoji rendered as ??— TextDecoder was called with the wrong encoding (e.g. "ascii" or "latin1"). Always use "utf-8".
- Blob shows empty image— MIME type mismatch. Extract it from the "data:...;base64," prefix, don't hardcode "image/png".
Key Facts
- Browser API:
- atob() — global, available since IE10
- Node.js API:
- Buffer.from(str, "base64").toString("utf-8")
- UTF-8 helper:
- TextDecoder — built-in, no polyfill needed
- URL-safe (Node):
- Use "base64url" encoding label (Node 16+)
- Image decoding:
- atob → Uint8Array → Blob → URL.createObjectURL
- Dependencies:
- Zero — all patterns above use only built-ins
Related Base64 Tools
- Base64 Encode JavaScript — the reverse direction with btoa()
- Base64 Decode Online — free browser tool
- Decode Base64 String — cross-language decode guide
- Base64 Image Decoder — data URL to Blob
- URL-Safe Base64 — for JWTs and query params
- JWT Debugger — inspect JWTs without writing code