How to Decode Base64 in JavaScript
Decode Base64 in JavaScript the right way: atob in the browser, Buffer in Node.js, Unicode-safe TextDecoder, URL-safe variants, and error handling.
Decoding Base64 in JavaScript looks like a one-liner — and for plain ASCII it is. But the moment you decode emoji, accented characters, a JWT payload, or a URL-safe string, the naive approach breaks in subtle, frustrating ways. This guide walks through every reliable method, in both the browser and Node.js, with the edge cases that actually bite developers in production.
If you just need a result right now without writing code, paste your string into Base64 Decode and read the plain text instantly. For everything else, here is how to do it properly in code.
The Quick Answer: atob()
Every browser (and modern Node.js) exposes two global functions:
btoa()— binary to aSCII (encode)atob()— aSCII to binary (decode)
atob('SGVsbG8sIFdvcmxkIQ=='); // "Hello, World!"
btoa('Hello, World!'); // "SGVsbG8sIFdvcmxkIQ=="
For simple ASCII text, this is all you need. The problem is that atob() returns a binary string — a string where each character represents one byte (0–255). That works perfectly for Latin characters, but it mangles anything outside the ASCII range.
The Unicode Problem
Try decoding a Base64 string that contains a non-ASCII character and you will see garbage:
// "Héllo" encoded as UTF-8 then Base64 is "SMOpbGxv"
atob('SMOpbGxv'); // "Héllo" ❌ mojibake
The accented é came out as two stray characters. Why? Because atob() gives you raw bytes, and é in UTF-8 is two bytes (0xC3 0xA9). Treating those two bytes as two separate characters produces the classic mojibake é.
The fix is to take the bytes from atob() and decode them as UTF-8 explicitly.
The modern, correct way: TextDecoder
function decodeBase64Utf8(b64) {
const binary = atob(b64);
const bytes = Uint8Array.from(binary, c => c.charCodeAt(0));
return new TextDecoder('utf-8').decode(bytes);
}
decodeBase64Utf8('SMOpbGxv'); // "Héllo" ✅
TextDecoder is built into every modern browser and Node.js. It is the recommended approach in 2024 and beyond — fast, standards-based, and correct for the full Unicode range including emoji.
// "🚀 launch" round-trips correctly
const bytes = new TextEncoder().encode('🚀 launch');
const b64 = btoa(String.fromCharCode(...bytes));
decodeBase64Utf8(b64); // "🚀 launch" ✅
Decoding Base64 in Node.js
Node.js has its own, cleaner API through the Buffer class. You do not need atob() at all (though it exists as a global since Node 16).
// Decode Base64 to a UTF-8 string
Buffer.from('SMOpbGxv', 'base64').toString('utf-8'); // "Héllo"
// Encode a UTF-8 string to Base64
Buffer.from('Héllo', 'utf-8').toString('base64'); // "SMOpbGxv"
Buffer handles Unicode correctly out of the box because you tell it the output encoding ('utf-8'). This is the idiomatic way to decode Base64 in any server-side JavaScript, including Express handlers, serverless functions, and build scripts.
Decoding to raw bytes (files, images)
When the Base64 represents binary data — an image, a PDF, a cryptographic key — do not convert it to a string. Keep it as a Buffer:
import { writeFileSync } from 'node:fs';
const b64 = 'iVBORw0KGgoAAAANSUhEUgAA...'; // a PNG, encoded
const buffer = Buffer.from(b64, 'base64');
writeFileSync('output.png', buffer); // writes the real image
Converting binary to a UTF-8 string would corrupt it, because most byte sequences are not valid UTF-8.
Handling URL-Safe Base64
JWTs, OAuth tokens, and many APIs use URL-safe Base64 (RFC 4648 §5), which swaps two characters and often drops padding:
| Standard | URL-safe |
|---|---|
+ |
- |
/ |
_ |
= padding |
usually omitted |
Native atob() and Buffer.from(..., 'base64') expect the standard alphabet, so you must normalize first:
function decodeBase64Url(b64url) {
// 1. Restore the standard alphabet
let b64 = b64url.replace(/-/g, '+').replace(/_/g, '/');
// 2. Re-add padding so length is a multiple of 4
while (b64.length % 4) b64 += '=';
// 3. Decode as UTF-8
const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
return new TextDecoder('utf-8').decode(bytes);
}
// Decode a JWT payload segment
const payload = 'eyJ1c2VyIjoiam9obiIsImFkbWluIjp0cnVlfQ';
decodeBase64Url(payload); // '{"user":"john","admin":true}'
Node 16+ also accepts the 'base64url' encoding directly, which handles all of this for you:
Buffer.from('eyJ1c2VyIjoiam9obiJ9', 'base64url').toString('utf-8');
// '{"user":"john"}'
If you want to inspect a token without writing code, the JWT decoder does the splitting and Base64URL decoding for you. To learn the bigger picture of how these formats relate, see encoding vs decoding.
Error Handling
atob() throws a DOMException ("InvalidCharacterError") when the input is not valid Base64. Always wrap decoding in a guard, especially when the input comes from users or external APIs:
function safeDecode(b64) {
try {
const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
} catch (err) {
return null; // invalid Base64 or invalid UTF-8
}
}
Passing { fatal: true } to TextDecoder makes it throw on malformed byte sequences instead of silently inserting replacement characters (�). That is usually what you want — fail loudly rather than ship corrupted text.
Common causes of decode failures:
- Whitespace or newlines in the string — strip them with
.replace(/\s/g, '')before decoding (MIME Base64 wraps lines at 76 characters). - Missing padding — re-add
=until the length is divisible by 4. - URL-safe characters (
-,_) passed to a standard decoder — normalize them first. - Double-encoded data — someone encoded an already-encoded string.
A Reusable, Production-Ready Helper
Here is a single function that handles standard and URL-safe input, whitespace, missing padding, and Unicode — suitable for both the browser and modern Node.js:
function decodeBase64(input) {
let b64 = input.trim()
.replace(/\s+/g, '') // strip MIME line breaks
.replace(/-/g, '+') // URL-safe → standard
.replace(/_/g, '/');
while (b64.length % 4) b64 += '='; // fix padding
const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
}
Drop this into any project and it will behave predictably against the messy real-world strings you actually receive.
FAQ
Why does atob() return weird characters for accented text or emoji?
Because atob() returns a binary string of raw bytes, not decoded text. Multi-byte UTF-8 characters (accents, emoji, CJK) come back as separate bytes and render as mojibake. Feed the bytes through TextDecoder('utf-8') to recover the original text.
Is atob() deprecated?
No. atob() and btoa() are still standard and supported everywhere. They are just low-level — they operate on binary strings, so you pair them with TextDecoder/TextEncoder for Unicode. In Node.js, Buffer is the more ergonomic choice.
How do I decode Base64 in Node.js without atob()?
Use Buffer.from(str, 'base64').toString('utf-8') for text, or keep the Buffer as-is for binary data like images and files. For URL-safe input, use the 'base64url' encoding (Node 16+).
How do I decode a JWT payload in JavaScript?
Split the token on ., take the second segment (the payload), and decode it as URL-safe Base64, then JSON.parse() the result. Note that this only reads the token — it does not verify the signature. Use the JWT decoder to inspect tokens visually.
What is the difference between standard and URL-safe Base64?
Standard Base64 uses + and / and pads with =. URL-safe Base64 replaces them with - and _ and usually omits padding, so the string can travel in URLs and filenames without escaping. Normalize URL-safe input back to the standard alphabet before decoding with atob().
How can I decode Base64 without writing any code?
Paste the string into Base64 Decode, and the plain text appears instantly in your browser. It handles whitespace, URL-safe characters, and missing padding automatically, with no data ever leaving your device.
Try it yourself
Use our free online tool to get started right away