Base64 shows up everywhere in JavaScript work: API tokens, data URLs for images, email attachments, even the odd query parameter. The browser ships with built-in functions to handle it, but there's one classic pitfall that trips up beginners and veterans alike. Let's walk through the correct way to decode (and encode) Base64 in JavaScript.
The basics: atob() and btoa()
Every modern browser (and Node.js) provides two global functions:
-
btoa()— binary to ASCII: encodes a string into Base64 -
atob()— ASCII to binary: decodes a Base64 string back into a plain string
const encoded = btoa("Hello, world!");
console.log(encoded); // SGVsbG8sIHdvcmxkIQ==
const decoded = atob(encoded);
console.log(decoded); // Hello, world!
That works great — until you try anything outside plain ASCII.
The UTF-8 pitfall
btoa() only accepts characters in the Latin1 range (code points 0–255). Pass in an emoji or any accented character and it throws an InvalidCharacterError:
btoa("héllo 🌍"); // ❌ throws InvalidCharacterError
And atob() has the mirror problem: it returns a string where each character is a single byte, so multi-byte UTF-8 sequences come back garbled:
atob("aMOpbGxvIPCfjI0="); // ❌ mojibake instead of "héllo 🌍"
The correct way: TextEncoder / TextDecoder
The fix is to convert through UTF-8 bytes explicitly. Here's a robust pair of helpers:
function base64Encode(str) {
const bytes = new TextEncoder().encode(str); // string → UTF-8 bytes
let binary = "";
bytes.forEach((b) => (binary += String.fromCharCode(b)));
return btoa(binary);
}
function base64Decode(b64) {
const binary = atob(b64); // Base64 → raw bytes as string
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
return new TextDecoder().decode(bytes); // UTF-8 bytes → string
}
base64Encode("héllo 🌍"); // aMOpbGxvIPCfjI0=
base64Decode("aMOpbGxvIPCfjI0="); // héllo 🌍 ✅
A practical example: decoding a JWT payload
JSON Web Tokens store their payload as Base64url-encoded JSON. Here's how to peek at one safely in the browser console:
function decodeJwtPayload(token) {
const base64url = token.split(".")[1];
const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
const binary = atob(base64);
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
return JSON.parse(new TextDecoder().decode(bytes));
}
Drop any unsigned JWT into that and you'll get its claims back as an object — handy when debugging API auth.
Don't want to write code every time?
For quick one-off decoding — pasting a token from a log file, checking a data URL, or sanity-checking an API response — it's faster to just use a tool. You can try it instantly at ToolStack Tools, a free Base64 decoder and encoder that runs entirely in your browser with no signup.
Recap
- Use
btoa()/atob()for plain ASCII strings. - Route through
TextEncoder/TextDecoderwhenever non-ASCII text is possible. - For JWTs, remember to convert Base64url back to standard Base64 first.
Happy decoding! 🎉
Top comments (0)