Base64 seems deceptively simple: encode binary data as ASCII text. Every developer uses it — embedding images, handling file uploads, passing data in URLs, dealing with JWTs. But there’s a surprising number of ways Base64 can silently break your application.
I’ve compiled the mistakes I’ve seen (and made) most often. Some of these are subtle enough that your code will appear to work fine until it hits a specific input that triggers the bug.
Quick Refresher: What Base64 Is and Isn’t
Base64 takes binary data and represents it using 64 printable ASCII characters (A-Z, a-z, 0-9, +, /) plus = for padding. It increases the size by about 33% — every 3 bytes of input become 4 characters of output.
Base64 is NOT encryption. This is covered in depth in Why Base64 Is Not Encryption, but it bears repeating: Base64 is an encoding, like converting between kilometers and miles. Anyone can decode it. Never use it to hide sensitive data.
Mistake #1: Standard Base64 in URLs
Standard Base64 uses + and / characters, which have special meaning in URLs:
+means “space” in URL query parameters/is a path separator
const token = btoa('user:admin?role=superuser');
// Result: "dXNlcjphZG1pbj9yb2xlPXN1cGVydXNlcg=="
// Put this in a URL and it breaks:
// https://api.example.com/auth?token=dXNlcjphZG1pbj9yb2xlPXN1cGVydXNlcg==
// The "==" at the end also causes problems in some URL parsers
The fix: Use Base64URL encoding. It replaces + with -, / with _, and strips padding =:
// Standard Base64 → Base64URL
function toBase64URL(base64) {
return base64
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
// Base64URL → Standard Base64
function fromBase64URL(base64url) {
let base64 = base64url
.replace(/-/g, '+')
.replace(/_/g, '/');
// Re-add padding
while (base64.length % 4 !== 0) {
base64 += '=';
}
return base64;
}
This is exactly what JWTs use. If you’ve ever looked at a JWT token, you’ll notice it never contains +, /, or = — that’s Base64URL at work.
Mistake #2: Double Encoding
This one’s insidious because the output looks like valid Base64, so you won’t notice until decoding produces garbage.
// The data is already Base64
const fromAPI = "SGVsbG8gV29ybGQ=";
// Developer accidentally encodes it again
const stored = btoa(fromAPI);
// Result: "U0dWc2JHOGV5Qnliblk5bFEk=" ← Base64 of Base64
// Later, decoding once gives you the first Base64 string, not the original data
const decoded = atob(stored);
// Result: "SGVsbG8gV29ybGQ=" ← still Base64!
Where this commonly happens:
- API responses that return Base64-encoded data, which you then encode again before storing
- File upload pipelines where the browser’s
FileReaderalready gives you Base64 - Copy-pasting between systems where encoding happens at multiple layers
The fix: Before encoding, check if the data is already Base64:
function isBase64(str) {
try {
return btoa(atob(str)) === str;
} catch {
return false;
}
}
This isn’t bulletproof (some non-Base64 strings can pass this check), but it catches the most common cases.
Mistake #3: Unicode and btoa()
JavaScript’s btoa() function only handles Latin-1 characters. Try to encode anything with Unicode characters beyond that range, and it throws:
btoa("Hello 🌍");
// DOMException: Failed to execute 'btoa': The string to be encoded
// contains characters outside of the Latin1 range.
The fix for modern browsers:
// Encode
function encodeUnicode(str) {
const bytes = new TextEncoder().encode(str);
const binary = Array.from(bytes, byte => String.fromCharCode(byte)).join('');
return btoa(binary);
}
// Decode
function decodeUnicode(base64) {
const binary = atob(base64);
const bytes = Uint8Array.from(binary, char => char.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
encodeUnicode("Hello 🌍"); // Works!
This is a common source of bugs in internationalized apps. Everything works during development with English text, then breaks in production when a Japanese or Arabic user triggers the code path.
Mistake #4: Ignoring Padding Issues
Base64 output length must be a multiple of 4. If it’s not, = characters are added as padding. Some systems strip this padding, others require it.
// With padding (standard)
btoa("ab") // "YWI="
btoa("abc") // "YWJj"
btoa("a") // "YQ=="
// Stripping padding (common in JWTs, URLs)
"YWI" // Valid in Base64URL, invalid in standard Base64
"YQ" // Same
The bug: If you strip padding for storage, then try to decode with a strict parser that expects padding, it fails silently or throws.
atob("YWI"); // Works in most browsers (lenient parsing)
atob("YQ"); // Works in most browsers
// But in Node.js:
Buffer.from("YWI", 'base64').toString(); // Works
// Some stricter parsers will reject this
The fix: Always normalize padding before decoding:
function addPadding(base64) {
const remainder = base64.length % 4;
if (remainder === 2) return base64 + '==';
if (remainder === 3) return base64 + '=';
return base64;
}
Mistake #5: Base64 for Large Files
I’ve seen developers Base64-encode entire files for upload:
// Reading a 10MB file as Base64
const reader = new FileReader();
reader.onload = () => {
// reader.result is now a ~13.3MB Base64 string
fetch('/upload', {
method: 'POST',
body: JSON.stringify({ file: reader.result }),
});
};
reader.readAsDataURL(file);
Problems with this approach:
- 33% size increase — a 10MB file becomes ~13.3MB
- Memory pressure — the entire file lives in memory as a string
- JSON parsing overhead — the server has to parse a massive JSON payload
- No streaming — you can’t track upload progress or resume on failure
The fix: Use FormData for file uploads:
const form = new FormData();
form.append('file', file);
fetch('/upload', {
method: 'POST',
body: form, // Sent as multipart/form-data, no Base64 needed
});
This is more efficient, supports streaming, and most server frameworks handle multipart form data natively.
When Base64 IS appropriate for files:
- Small files (icons, thumbnails under ~10KB)
- Embedding images in JSON APIs where multipart isn’t an option
- Data URIs in CSS or HTML (
data:image/png;base64,...)
Mistake #6: Inconsistent Line Breaks
The Base64 spec (RFC 2045) for MIME actually requires line breaks every 76 characters. Some encoders add them, some don’t. This can cause mismatches:
// With line breaks (MIME-style)
SGVsbG8gV29ybGQhIFRoaXMgaXMgYSBsb25nZXIgc3RyaW5nIHRoYXQgd2ls
bCB3cmFwIGFjcm9zcyBtdWx0aXBsZSBsaW5lcyBpbiBNSU1FIGZvcm1hdC4=
// Without line breaks (web-style)
SGVsbG8gV29ybGQhIFRoaXMgaXMgYSBsb25nZXIgc3RyaW5nIHRoYXQgd2lsbCB3cmFwIGFjcm9zcyBtdWx0aXBsZSBsaW5lcyBpbiBNSU1FIGZvcm1hdC4=
Where this bites you: Email systems (SMTP) use MIME Base64 with line breaks. Web APIs use continuous Base64 without breaks. If you pass MIME-encoded Base64 to a web decoder, the \n characters cause a decoding failure.
The fix: Strip line breaks and whitespace before decoding:
function cleanBase64(encoded) {
return encoded.replace(/[\s\n\r]/g, '');
}
A Real-World Debugging Session
Last year, I was debugging a file preview feature that worked for most uploads but occasionally showed a corrupted image. The bug was a combination of mistakes #1 and #4:
- The backend returned file data as standard Base64 (with
+and/) - The frontend stored it in a URL parameter for the preview page
- The
+characters were being decoded as spaces by the URL parser - Most files worked because they happened not to have
+in their Base64 representation - Files that contained certain byte patterns produced Base64 with
+, and those broke
The fix was switching to Base64URL encoding on the backend. Two lines of code, but finding the root cause took the better part of an afternoon.
Test Your Encoding
If you’re working with Base64 and want to quickly test encoding/decoding without firing up a REPL, our Base64 Encoder/Decoder handles both standard and URL-safe variants. It’s useful for verifying that your code’s output matches what you expect, especially when debugging the kinds of issues described above.
For URL-encoding edge cases (which often intersect with Base64 bugs), the URL Encoder shows you exactly what characters get escaped and why.
This article is part of our Encoding & Hashing Guide. You might also want to read Why Base64 Is Not Encryption — a surprisingly common misconception.