You've got a Base64-encoded string — an API response, a JWT payload, a data URL — and you need the original text back. Here's every way to decode Base64 in JavaScript, including the gotchas that will silently corrupt your data if you hit them.
Quick answer: In a browser, use atob(encoded). In Node.js, use Buffer.from(encoded, 'base64').toString('utf-8'). Neither handles Unicode properly on its own — keep reading if your data includes anything beyond basic ASCII.
To decode interactively without writing code, use the Base64 decoder.
Open Tool →What Base64 Decoding Does
Base64 encoding converts binary data into 64 printable ASCII characters (A–Z, a–z, 0–9, +, /). Decoding reverses this: it takes that ASCII string and reconstructs the original bytes.
The output of atob() is a byte string — not necessarily a readable text string. Whether the result is meaningful text depends on what was originally encoded. If the original was UTF-8 text, you need an extra step to handle multi-byte characters correctly. More on that in the Unicode section below.
Method 1: atob() in the Browser
atob() is the native browser function for Base64 decoding. It's available in all modern browsers and has been since IE10.
// JavaScript (Browser)
const encoded = "SGVsbG8sIFdvcmxkIQ==";
const decoded = atob(encoded);
console.log(decoded); // Hello, World!
atob stands for "ASCII to binary" — a naming convention inherited from old Unix tooling. btoa() goes the other direction (binary to ASCII, i.e., encode).
Important: atob() also works in Node.js 16+. But for Node.js, Buffer is still the better choice — it handles UTF-8 correctly without extra steps.
Method 2: Buffer in Node.js
In Node.js, the Buffer class is the standard approach. It handles UTF-8 encoding correctly out of the box.
// Node.js
const encoded = "SGVsbG8sIFdvcmxkIQ==";
const decoded = Buffer.from(encoded, "base64").toString("utf-8");
console.log(decoded); // Hello, World!
To encode (the reverse operation):
// Node.js
const original = "Hello, World!";
const encoded = Buffer.from(original, "utf-8").toString("base64");
console.log(encoded); // SGVsbG8sIFdvcmxkIQ==
Buffer.from() takes two arguments: the data, and the encoding of that data. Passing "base64" tells Node.js to treat the input as Base64 and decode it to raw bytes. .toString("utf-8") then converts those bytes to a UTF-8 string.
Method 3: Unicode and UTF-8 — The Gotcha
atob() has a critical limitation: it only handles bytes with code points up to 0xFF. If the original string contained multi-byte UTF-8 characters — emoji, Chinese characters, accented letters — atob() will either throw an error or silently produce garbage.
Example of silent corruption:
// This will NOT work correctly for non-ASCII characters
const encoded = btoa("café"); // Throws: "btoa: The string to be encoded contains characters outside of the Latin1 range."
The failure isn't always an error. If someone else encoded the string with a UTF-8-aware method and you decode with plain atob(), the output looks like text but the characters are wrong.
The Fix: TextDecoder + Uint8Array
The modern browser solution uses TextDecoder, which understands UTF-8 natively:
// JavaScript (Browser) — UTF-8 safe decode
function base64ToBytes(base64) {
const binString = atob(base64);
return Uint8Array.from(binString, (c) => c.codePointAt(0));
}
function decodeBase64UTF8(base64) {
return new TextDecoder().decode(base64ToBytes(base64));
}
// Example
const encoded = "YSDEgCDwkICAIOaWhyDwn6aE";
console.log(decodeBase64UTF8(encoded)); // a Ā 𐀀 文 🦄
This is the pattern recommended by MDN. The flow: atob() converts Base64 to a binary string → Uint8Array.from() converts that to bytes → TextDecoder interprets the bytes as UTF-8.
In Node.js you don't need this — Buffer.from(encoded, 'base64').toString('utf-8') handles it correctly.
Method 4: Uint8Array.fromBase64() — The New API
Uint8Array.fromBase64() is a native JavaScript method added in ES2026 (Chrome 130+, Firefox 133+, Safari 18.2+). It's cleaner than the atob() + manual conversion approach:
// JavaScript (Modern Browser / Node.js 22.4+)
const encoded = "SGVsbG8sIFdvcmxkIQ==";
const bytes = Uint8Array.fromBase64(encoded);
const decoded = new TextDecoder().decode(bytes);
console.log(decoded); // Hello, World!
To check if it's available before using it:
if (typeof Uint8Array.fromBase64 === "function") {
// Use new API
} else {
// Fall back to atob() + TextDecoder pattern
}
The new API also supports Base64URL encoding directly via an options argument (see Base64URL section below).
Decoding Base64URL (Used in JWTs)
Base64URL is a variant of Base64 that replaces + with - and / with _, making it safe for use in URLs without percent-encoding. It also omits the = padding. JWTs use Base64URL for their header and payload segments.
atob() does not handle Base64URL input — the - and _ characters will cause it to throw. Fix this before decoding:
// JavaScript (Browser)
function decodeBase64URL(base64url) {
// Convert Base64URL to standard Base64
const base64 = base64url
.replace(/-/g, "+")
.replace(/_/g, "/");
// Restore padding
const padded = base64.padEnd(base64.length + (4 - (base64.length % 4)) % 4, "=");
return atob(padded);
}
// Decode a JWT payload (middle segment)
const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphbmUgRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
const payload = jwt.split(".")[1];
const decoded = JSON.parse(decodeBase64URL(payload));
console.log(decoded);
// { sub: "1234567890", name: "Jane Doe", iat: 1516239022 }
With the new Uint8Array.fromBase64() API, Base64URL is handled natively:
// JavaScript (Modern Browser / Node.js 22.4+)
const bytes = Uint8Array.fromBase64(base64url, { alphabet: "base64url" });
const decoded = new TextDecoder().decode(bytes);
You can decode JWT tokens interactively without writing code.
Open Tool →Handling Malformed Input and Errors
atob() throws a DOMException with message "Invalid character" if the input contains characters not in the Base64 alphabet, or if the string length is not a valid Base64 length.
Always wrap atob() in a try/catch for untrusted input:
// JavaScript (Browser)
function safeDecodeBase64(encoded) {
try {
return atob(encoded.trim());
} catch (e) {
console.error("Invalid Base64:", e.message);
return null;
}
}
Common causes of invalid Base64:
- Missing padding: Some systems strip the
=padding. Fix:base64.padEnd(base64.length + (4 - (base64.length % 4)) % 4, "=") - Whitespace: Copy-pasted Base64 sometimes includes line breaks. Fix:
encoded.replace(/\s/g, "") - Base64URL characters:
-and_aren't valid in standard Base64. Fix: convert first as shown above.
Quick Reference by Environment
| Environment | Method | Unicode-Safe? |
|---|---|---|
| Browser (ASCII only) | atob(encoded) |
No |
| Browser (Unicode) | atob() + Uint8Array + TextDecoder |
Yes |
| Browser (modern, 2024+) | Uint8Array.fromBase64(encoded) |
Yes (with TextDecoder) |
| Node.js | Buffer.from(encoded, 'base64').toString('utf-8') |
Yes |
Frequently asked questions
Does atob() work in Node.js?
Yes, since Node.js 16. But Buffer.from(encoded, 'base64').toString('utf-8') is still preferred in Node.js — it handles UTF-8 correctly without extra steps.
Why does my decoded string look like garbage?
The original data was UTF-8 encoded before being Base64 encoded, but you decoded with plain atob(). Use the TextDecoder approach (or Buffer in Node.js) to get correct UTF-8 output.
What's Base64URL and why does atob() fail on it?
Base64URL replaces + with - and / with _ for URL safety. atob() only accepts standard Base64 characters. Convert - → + and _ → / before calling atob(), and restore the = padding.
Is Base64 a form of encryption?
No. Base64 is encoding, not encryption. Anyone can decode a Base64 string without a key. Don't use it to protect sensitive data — use it to represent binary data as text.
What throws "Invalid character" in atob()?
Any character not in A-Za-z0-9+/=. Common culprits: Base64URL characters (-, _), whitespace in copy-pasted strings, and data URIs that include a prefix (data:image/png;base64,... — strip the prefix first).
Summary
Base64 decoding in JavaScript comes down to your environment and what you're decoding:
- Browser, plain ASCII:
atob(encoded)is all you need. - Browser, UTF-8 text: Use
atob()+Uint8Array+TextDecoder, orUint8Array.fromBase64()in modern browsers. - Node.js:
Buffer.from(encoded, 'base64').toString('utf-8')handles everything. - JWTs / Base64URL: Convert
-→+and_→/, restore padding, then decode. - Untrusted input: Always wrap in try/catch.
For one-off decoding while debugging, skip the code entirely — paste your string into the online Base64 decoder and get the result instantly.