Base64 Decode Shows Garbled Text — UTF-8 and Wrong Charset

(Updated: July 16, 2026 ) base64 UTF-8 encoding

Paste JWT payload into an online decoder — get {"name":"å±±ç°å¤ªéƒŽ"} instead of kanji. Paste API response into atob() — diamond question marks everywhere. r/learnprogramming threads blame “base64 is broken” when the real issue is charset pipeline or you decoded JPEG bytes as UTF-8.

  • atob() returns UTF-8 text — It returns a “binary string” where each char is a byte 0–255. Multi-byte UTF-8 sequences split across char codes → mojibake unless you use TextDecoder.
  • “Just use decodeURIComponent(escape(atob(s)))” — Legacy hack; works sometimes, fails on invalid surrogate pairs. Use TextDecoder in modern browsers.
  • Assuming all base64 is text — Images, PDFs, protobuf — decoding as string will always look “garbled.”
  • URL-safe base64- and _ variants need normalization before decode; padding = often dropped in JWTs.

Correct UTF-8 decode in browser

function base64ToUtf8(b64) {
  const binary = atob(b64.replace(/-/g, "+").replace(/_/g, "/"));
  const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
  return new TextDecoder("utf-8").decode(bytes);
}

Node.js:

Buffer.from(b64, "base64").toString("utf8");

Common mojibake patterns

You seeLikely cause
é instead of éUTF-8 bytes interpreted as Latin-1 / Windows-1252
Replacement char — invalid UTF-8 sequence
å±± style CJK garbageDouble-encoding or wrong decoder
Random symbols + %You decoded compressed/binary data

Double UTF-8: Text was UTF-8, someone ran it through Latin-1 decoder, re-encoded — fix at source, not with another guess decode.

JWT / JSON payloads

JWT middle segment is base64url (no padding). After decode you should get valid JSON — if JSON.parse fails but string “looks almost English,” check:

  1. Padding — add = until length % 4 === 0
  2. URL-safe chars — swap -/_ to +//
  3. UTF-8 names in claims — use TextDecoder path above

Use jwt decode tool for header/payload split without running token through sketchy pastebins.

When it’s not encoding — bad input

  • Truncated copy (Slack/email line wrap)
  • Whitespace/newlines inside string — strip before decode
  • Data URL prefix still attached — data:image/png;base64, must be removed for raw decode
  • Hex or base64 confusion — different alphabets

Binary vs text decision tree

Decode base64 → bytes
  ├─ Magic bytes PNG/GIF/PDF? → binary file, don't stringify
  ├─ Valid UTF-8 text? → TextDecoder utf-8
  └─ Still garbled → ask source for charset or raw bytes

Server-side gotcha

PHP base64_decode + echo without Content-Type: charset=utf-8 — browser guesses wrong charset. Python .decode('utf-8') vs .decode('latin-1') — pick explicitly.

Email and MIME layers

SMTP often wraps bodies in quoted-printable or base64 Content-Transfer-Encoding inside MIME multipart. Decoding the outer base64 blob gives you another layer (headers + nested parts) — not plain user text. Mail parser libraries handle this; raw atob on an .eml paste won’t.

Spreadsheet and CSV exports

Excel sometimes base64-embeds small images or stores UTF-16LE blobs. Pasting into a web decoder shows noise because the payload was never UTF-8 text. Export as CSV UTF-8 or use the app’s native export before debugging encoding.

Tool: encode/decode safely

The base64 tool handles encode/decode in-browser — useful for quick UTF-8 checks without shipping secrets to random websites.

Try the base64 tool

TL;DR

Base64 is transport, not charset. Use bytes → TextDecoder for UTF-8. Garbled CJK is usually Latin-1 misread or double encoding. Binary payloads aren’t supposed to read as English.