Logo
Back to Blog
Development August 8, 2026 9 min read

Encode and Decode Base64 in JavaScript, Python, PHP and Bash

O

OmniWebKit Team

Developer Tooling

Share:
Article Cover Image

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

EnvironmentEncodeThe thing that bites
Browser JSbtoaThrows on any non-Latin-1 character
Node.jsBuffer.fromNone significant
Pythonbase64.b64encodeReturns bytes, not a string
PHPbase64_encodeDecoding fails silently without strict mode
Bashbase64Wraps 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.

Frequently Asked Questions

Why does btoa throw InvalidCharacterError on my string?

+
Because btoa only accepts characters in the Latin-1 range, roughly the first 256 code points. Any emoji, Chinese character, or curly quote pasted from a word processor exceeds that. Convert the text to UTF-8 bytes with TextEncoder first.

Why does Python print a b prefix on my result?

+
Because b64encode returns bytes, not a string. The b is Python telling you which type it handed back. Call decode with ascii on the result when you need a normal string.

Is base64_decode in PHP safe on untrusted input?

+
It will not crash, but it fails quietly, which is worse. Invalid characters are skipped silently unless you pass true as the second argument for strict mode. Always use strict mode on anything user-supplied.

Why does my terminal output have line breaks?

+
The base64 command wraps at 76 characters by default, following the MIME spec. Pass -w 0 on Linux to disable it. macOS ships a different implementation where the flag is -b, or you can pipe through tr to strip newlines.

Should I use Buffer or btoa in Node.js?

+
Buffer, always. btoa exists in newer Node versions for browser compatibility but carries the same Latin-1 limitation. Buffer handles any encoding you name and is the idiomatic choice.

How do I encode a file rather than a string?

+
Read it as bytes first, then encode those bytes. In Python that is open with rb mode, in Node it is readFileSync, in PHP it is file_get_contents. Reading as text corrupts binary data before encoding even starts.

Why does decoding give me a different string than I encoded?

+
Almost always a character encoding mismatch. If you encoded UTF-8 bytes and decoded assuming Latin-1, multi-byte characters come apart. Be explicit about the encoding on both sides.

Is there a streaming approach for very large files?

+
Yes, and it is worth using past a few hundred megabytes. Node has base64 stream transforms, and Python can chunk with b64encode as long as each chunk is a multiple of three bytes. Chunking at any other boundary inserts stray padding mid-stream.

How do I handle base64url in each language?

+
Python has urlsafe_b64encode built in. Node accepts base64url as an encoding name from version 16. JavaScript in the browser and PHP both need a manual character swap: plus becomes dash, slash becomes underscore.

Why is my decoded PDF corrupted only sometimes?

+
Look for whitespace in the input. Newlines, tabs, and stray spaces survive a copy-paste and some decoders choke on them while others strip them. Remove all whitespace before decoding and the intermittent failures usually stop.

Tags

#JavaScript#Python#PHP#Base64#Bash