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, or Uint8Array.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.