Base64 encode and decode in JavaScript (UTF-8 safe)
A snip.tools guide · runs alongside the Base64 encode / decode
Prefer to just run it? Use the free Base64 encode / decode — no install, in your browser.
Open the Base64 encode / decode
JavaScript has had Base64 built in for years — btoa() to encode, atob() to
decode. The catch is that they predate Unicode-aware strings and quietly break the moment your text contains
an accent or an emoji. Here's why, the few lines that fix it for good, and a more forgiving decoder for
real-world input. (It's the same approach our in-browser Base64 encoder & decoder
uses.)
Why btoa() and atob() aren't enough
btoa ("binary to ASCII") expects a string where every character fits in one byte — the Latin-1
range. Pass it anything outside that range and it throws:
// The built-ins — fine for ASCII, broken for everything else:
btoa('hello'); // "aGVsbG8=" ✅
atob('aGVsbG8='); // "hello" ✅
btoa('héllo'); // ❌ InvalidCharacterError
btoa('🚀 launch'); // ❌ throws — emoji aren't Latin-1
The fix isn't to abandon btoa/atob — they're the right low-level primitives. It's
to convert your text to UTF-8 bytes first, so every character is represented as bytes
btoa can handle, and to reverse that on the way back.
UTF-8-safe encoding
TextEncoder turns a string into its UTF-8 bytes. Pack those bytes into a binary string,
then Base64 it:
// Encode any string (UTF-8 safe) → Base64
function base64Encode(str) {
const bytes = new TextEncoder().encode(str); // string → UTF-8 bytes
let binary = '';
for (const b of bytes) binary += String.fromCharCode(b);
return btoa(binary);
}
base64Encode('🚀 launch'); // "8J+agCBsYXVuY2g=" UTF-8-safe decoding
Decoding is the mirror image: atob gives you a binary string, you read its char codes back into
a byte array, and TextDecoder turns those UTF-8 bytes back into a proper string:
// Decode Base64 → string (UTF-8 safe)
function base64Decode(b64) {
const binary = atob(b64);
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
base64Decode('8J+agCBsYXVuY2g='); // "🚀 launch" These two functions round-trip any string — accents, CJK characters, emoji — correctly, because the bytes in the middle are real UTF-8.
A tolerant decoder for real-world input
Base64 you receive from the wild isn't always clean. It may arrive wrapped across lines (email bodies), in
the URL-safe alphabet that swaps +// for -/_
(used by JWTs), or with its = padding stripped. A strict atob chokes on all three.
A few .replace() calls make the decoder forgiving:
// A forgiving decoder: handles line-wrapped input,
// the URL-safe alphabet (-/_), and missing padding.
function base64DecodeTolerant(b64) {
let s = b64
.replace(/\s+/g, '') // strip newlines/spaces (wrapped email bodies)
.replace(/-/g, '+') // URL-safe → standard
.replace(/_/g, '/');
const rem = s.length % 4;
if (rem) s += '='.repeat(4 - rem); // restore padding
const bytes = Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
return new TextDecoder().decode(bytes);
} This normalises the input before decoding, so a JWT segment or a line-wrapped blob decodes cleanly while genuinely invalid data still throws — exactly what you want. Note that Base64 has no checksum, so a string that was never valid Base64 will simply decode to garbage rather than error.
On the server: Node.js
In Node you don't need any of the above — the Buffer API is UTF-8 safe out of the box:
// Node.js: the Buffer API is simplest and UTF-8 safe by default.
const b64 = Buffer.from('🚀 launch', 'utf8').toString('base64');
const txt = Buffer.from(b64, 'base64').toString('utf8');
Use Buffer on the server and the TextEncoder/TextDecoder pair in the
browser. One last reminder that applies everywhere: Base64 is an encoding, not encryption — it
hides nothing and anyone can decode it, so never use it to protect secrets. For putting data into a URL
rather than a byte stream, reach for URL encoding instead.
Try it now: the Base64 encode / decode does everything in this guide in your browser — nothing is uploaded. Browse more guides or the full tool list.