You find a Base64 snippet on Stack Overflow, paste it in, and it throws an error on the first emoji. Or it works locally and corrupts files in production. Every language has its own trap here, and they are all avoidable.
Here is working code for four environments, plus the specific thing that bites in each.
Base64 Decode in JavaScript, and the Unicode Trap
The browser gives you two functions. Both are older than Unicode support, and it shows.
btoa('Hello'); // 'SGVsbG8='
atob('SGVsbG8='); // 'Hello'
Now try an emoji and it throws InvalidCharacterError. The reason is specific: btoa only accepts characters up to code point 255.
The fix is to convert to UTF-8 bytes first:
function encode(str) {
const bytes = new TextEncoder().encode(str);
let bin = '';
bytes.forEach(b => { bin += String.fromCharCode(b); });
return btoa(bin);
}
function decode(b64) {
const bin = atob(b64.replace(/\s/g, ''));
const bytes = Uint8Array.from(bin, c => c.charCodeAt(0));
return new TextDecoder('utf-8').decode(bytes);
}
Note the whitespace strip in the decoder. Pasted strings often carry newlines, and that alone causes failures people spend an afternoon on.
This is exactly what our Base64 encoder and decoder does internally, which is why emoji round-trip there.
Node.js: Use Buffer, Not btoa
Server-side JavaScript has none of the browser's baggage. Buffer handles it in one line:
const encoded = Buffer.from('Hello 👋', 'utf8').toString('base64');
const decoded = Buffer.from(encoded, 'base64').toString('utf8');
// Files need no special handling at all
const fileB64 = require('fs').readFileSync('doc.pdf').toString('base64');
Newer Node versions expose btoa for browser parity. Ignore it — it carries the same Latin-1 limit and exists only for portability.
From Node 16 you can also pass base64url as the encoding name, which saves a manual character swap.
Python base64 Encode: Watch the Bytes
Python's module is clean, and its one sharp edge is the type system.
import base64
encoded = base64.b64encode('Hello'.encode('utf-8'))
print(encoded) # b'SGVsbG8='
print(encoded.decode('ascii')) # SGVsbG8=
decoded = base64.b64decode('SGVsbG8=').decode('utf-8')
That b prefix confuses everyone once. b64encode returns bytes, not a string, and Python is telling you so.
For files, open in binary mode. Text mode will corrupt the data before encoding starts:
with open('doc.pdf', 'rb') as f:
encoded = base64.b64encode(f.read()).decode('ascii')
Python also ships urlsafe_b64encode and urlsafe_b64decode, which handle the URL-safe variant with no manual swapping.
PHP base64_decode and Its Silent Failure
PHP is the simplest of the four, and the most dangerous by default.
$encoded = base64_encode('Hello'); // SGVsbG8=
$decoded = base64_decode($encoded); // Hello
// Files
$fileB64 = base64_encode(file_get_contents('doc.pdf'));
Here is the caveat we flag in every code review: base64_decode silently skips invalid characters. Feed it garbage and you get partial output rather than an error.
Pass true as the second argument to get strict mode, which returns false on invalid input:
$decoded = base64_decode($input, true);
if ($decoded === false) {
throw new InvalidArgumentException('Not valid Base64');
}
Always use strict mode on anything a user supplied. A quiet partial decode is far harder to debug than a thrown exception.
Base64 on the Command Line in Linux and macOS
Useful, and the source of the most common cross-platform surprise.
# Encode a file
base64 document.pdf > encoded.txt
# Decode it back
base64 -d encoded.txt > document.pdf
# Encode a string
echo -n 'Hello' | base64
That -n matters. Without it, echo appends a newline and you encode a byte you did not intend.
The bigger trap is line wrapping. GNU base64 wraps output at 76 characters by default:
# Linux: one unbroken line
base64 -w 0 document.pdf
# macOS uses a different flag
base64 -b 0 document.pdf
Paste wrapped output into a JSON field and it breaks. We have watched this cost a whole afternoon more than once.
Which Trap Bites Where
| Environment | Encode | The thing that bites |
|---|---|---|
| Browser JS | btoa | Throws on any non-Latin-1 character |
| Node.js | Buffer.from | None significant |
| Python | base64.b64encode | Returns bytes, not a string |
| PHP | base64_encode | Decoding fails silently without strict mode |
| Bash | base64 | Wraps at 76 characters by default |
Two patterns show up across all five. Be explicit about character encoding, and strip whitespace before decoding.
Wrapping Up
Base64 decode in JavaScript needs a UTF-8 step the built-in functions do not give you. Node, Python, and PHP are all one-liners with one gotcha each, and the command line needs a flag nobody remembers.
When something fails, check three things first: character encoding, stray whitespace, and whether the string was truncated.
For a quick check without writing code, paste the string into our Base64 to file decoder — it names the format it found and flags truncation. The background is in what Base64 encoding is.
