The Two-Step Decode Model
Regardless of language, decoding a Base64 string is always a two-step process:
- Base64 → bytes — the decoder converts the Base64 alphabet into the original byte sequence.
- bytes → text — if you expect a human-readable string, you must additionally decode those bytes as UTF-8 (or whichever encoding was used before Base64-encoding).
Most decoding bugs come from skipping step 2 or getting the encoding wrong.
JavaScript
// Simple ASCII case
const decoded = atob('SGVsbG8gV29ybGQ=');
// → "Hello World"
// UTF-8 safe (recommended for anything modern)
function decodeBase64ToString(str) {
const bytes = Uint8Array.from(atob(str), c => c.charCodeAt(0));
return new TextDecoder('utf-8').decode(bytes);
}
decodeBase64ToString('SGVsbG8g5LiW55WMIPCfkYs=');
// → "Hello 世界 👋"
// Node.js (server-side)
Buffer.from('SGVsbG8=', 'base64').toString('utf-8');
// → "Hello"Python
import base64
# Standard decode
decoded_bytes = base64.b64decode('SGVsbG8gV29ybGQ=')
decoded_text = decoded_bytes.decode('utf-8')
print(decoded_text) # "Hello World"
# URL-safe decode (with or without padding)
def urlsafe_decode(s):
# Add missing padding
padded = s + '=' * (-len(s) % 4)
return base64.urlsafe_b64decode(padded).decode('utf-8')Java
import java.util.Base64;
import java.nio.charset.StandardCharsets;
// Standard
byte[] decoded = Base64.getDecoder().decode("SGVsbG8gV29ybGQ=");
String text = new String(decoded, StandardCharsets.UTF_8);
System.out.println(text); // "Hello World"
// URL-safe
byte[] urlDecoded = Base64.getUrlDecoder().decode("SGVsbG8_");C# / .NET
using System;
using System.Text;
byte[] decoded = Convert.FromBase64String("SGVsbG8gV29ybGQ=");
string text = Encoding.UTF8.GetString(decoded);
Console.WriteLine(text); // "Hello World"
// For URL-safe: replace chars and add padding first
string urlSafeToStandard(string s) =>
s.Replace('-', '+').Replace('_', '/').PadRight(s.Length + (4 - s.Length % 4) % 4, '=');Go
package main
import (
"encoding/base64"
"fmt"
)
func main() {
// Standard
decoded, err := base64.StdEncoding.DecodeString("SGVsbG8gV29ybGQ=")
if err != nil { panic(err) }
fmt.Println(string(decoded)) // "Hello World"
// URL-safe (with or without padding)
decoded2, _ := base64.RawURLEncoding.DecodeString("SGVsbG8_")
fmt.Println(string(decoded2))
}PHP
<?php
// Standard
$decoded = base64_decode('SGVsbG8gV29ybGQ=');
echo $decoded; // "Hello World"
// Strict mode — fail on invalid characters
$decoded = base64_decode($input, true);
if ($decoded === false) {
die('Invalid Base64');
}
// URL-safe helper
function base64url_decode($str) {
$padded = str_pad(strtr($str, '-_', '+/'), strlen($str) % 4, '=', STR_PAD_RIGHT);
return base64_decode($padded);
}Rust
// Cargo.toml: base64 = "0.22"
use base64::{Engine as _, engine::general_purpose};
fn main() {
let decoded = general_purpose::STANDARD
.decode("SGVsbG8gV29ybGQ=").unwrap();
let text = String::from_utf8(decoded).unwrap();
println!("{}", text); // "Hello World"
// URL-safe, no padding
let decoded2 = general_purpose::URL_SAFE_NO_PAD
.decode("SGVsbG8").unwrap();
}Bash / Shell
# Simple decode
echo "SGVsbG8gV29ybGQ=" | base64 -d
# → Hello World
# From a file
base64 -d < encoded.txt > decoded.bin
# URL-safe → standard, then decode
echo "SGVsbG8_" | tr '_-' '/+' | base64 -d 2>/dev/null
# Decode a JWT payload (middle segment, add padding first)
JWT_PAYLOAD="eyJ1c2VyIjoiaGVsbG8ifQ"
padded="$JWT_PAYLOAD$(printf '=%.0s' $(seq 1 $((4 - ${#JWT_PAYLOAD} % 4))))"
echo "$padded" | tr '_-' '/+' | base64 -dCommon Base64 Decode Errors
- "Invalid character" / "bad base64 input" — extra whitespace, newlines, or non-Base64 characters in the input. Strip them first:
str.replace(/\s+/g, '')in JS,"".join(s.split())in Python. - Length not a multiple of 4 — padding was stripped. Add
=characters back to make the length divisible by 4. - Result has weird accented characters — you decoded raw bytes as Latin-1 instead of UTF-8. Always specify UTF-8 explicitly.
- Empty result — either the input was empty, or your language returned
null/None/falsebecause strict validation failed. Check for validation errors and inspect the raw input. - Different result in different languages — one is using standard, another URL-safe. Normalize before comparing.
Key Facts
- Input alphabet:
- 64 chars: A-Z, a-z, 0-9, +, / (standard) or -, _ (URL-safe)
- Padding:
- = character to make length divisible by 4 (may be stripped)
- Decode ratio:
- 3 bytes out for every 4 chars in (roughly 75% size reduction)
- Bytes vs text:
- Decoder returns bytes — you interpret as UTF-8/Latin-1/binary
- Lossless:
- Yes — encode-then-decode always recovers the exact bytes
- Not encryption:
- Base64 is encoding, not encryption. Anyone can decode it.
Related Base64 Tools
- Base64 Decode Online — general-purpose browser decoder
- Base64 Decode JavaScript — deep-dive on atob() + TextDecoder
- Base64 Decode Python — b64decode() patterns
- Base64 Encode String — reverse direction
- URL-Safe Base64 — for JWTs and query params
- JWT Debugger — decode structured JWT tokens