How to Decode Base64 in JavaScript (Browser & Node.js)
Introduction to Base64 in JavaScript
JavaScript provides native support for Base64 encoding and decoding through the btoa() and atob() functions. These functions have been part of the Web API for over a decade and are universally supported across all modern browsers and Node.js environments.
However, despite their simplicity, working with Base64 in JavaScript is notoriously error-prone when dealing with internationalization. Because the atob() engine was designed around legacy Latin-1 string processing, it fundamentally fails when presented with modern UTF-8 multi-byte characters like emojis or Chinese symbols. In this guide, we will explore both the basic implementation and the robust, production-ready solution using the TextDecoder API.
The Basic Native Functions: btoa() and atob()
The names of these functions are historically derived from "binary to ASCII" (btoa) and "ASCII to binary" (atob).
Encoding Text (btoa)
const text = "Hello World";
const encoded = btoa(text);
console.log(encoded); // Output: "SGVsbG8gV29ybGQ="
Decoding Text (atob)
const encoded = "SGVsbG8gV29ybGQ=";
const decoded = atob(encoded);
console.log(decoded); // Output: "Hello World"
If you are strictly working with basic English letters and numbers (ASCII characters 0-127), these functions work perfectly. However, the moment you step outside this range, you will encounter the dreaded InvalidCharacterError.
The UTF-8 Problem
Let's look at what happens when you try to encode a string containing a simple emoji:
const text = "Hello 🚀";
// Uncaught DOMException: Failed to execute 'btoa' on 'Window':
// The string to be encoded contains characters outside of the Latin1 range.
const encoded = btoa(text);
JavaScript strings are encoded in UTF-16 internally. The btoa() function expects a binary string where every character represents a single 8-bit byte. When it encounters the rocket emoji (which is composed of multiple bytes), it throws an error because it cannot safely pack those large values into the Base64 alphabet.
The Production-Ready Solution: TextEncoder and TextDecoder
To safely handle any Unicode character, we must first convert the JavaScript string into a raw array of 8-bit UTF-8 bytes, and then convert those bytes into a Base64 string. We achieve this using the modern TextEncoder and TextDecoder APIs.
Robust UTF-8 Encoding in JavaScript
function encodeBase64UTF8(text) {
// 1. Convert string to a Uint8Array of UTF-8 bytes
const encoder = new TextEncoder();
const bytes = encoder.encode(text);
// 2. Convert the byte array into a binary string
let binaryString = '';
for (let i = 0; i < bytes.byteLength; i++) {
binaryString += String.fromCharCode(bytes[i]);
}
// 3. Base64 encode the binary string
return btoa(binaryString);
}
console.log(encodeBase64UTF8("Hello 🚀"));
// Output: "SGVsbG8g8J+agA=="
Robust UTF-8 Decoding in JavaScript
To reverse the process safely, we decode the Base64 back into a binary string, convert that string into a Uint8Array, and then use TextDecoder to safely parse the multi-byte characters.
function decodeBase64UTF8(encoded) {
// 1. Decode Base64 to binary string
const binaryString = atob(encoded);
// 2. Convert binary string to a Uint8Array
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
// 3. Decode the UTF-8 bytes back into a JavaScript string
const decoder = new TextDecoder('utf-8');
return decoder.decode(bytes);
}
console.log(decodeBase64UTF8("SGVsbG8g8J+agA=="));
// Output: "Hello 🚀"
Handling URL-Safe Base64 Variants
When working with JSON Web Tokens (JWTs) or routing parameters, you will often encounter Base64URL strings. These strings use hyphens (-) instead of pluses (+), and underscores (_) instead of slashes (/). If you pass a Base64URL string directly into atob(), it will crash.
You must sanitize the string before decoding:
function sanitizeForDecode(base64UrlString) {
// Replace URL-safe characters with standard Base64 characters
let standardBase64 = base64UrlString.replace(/-/g, '+').replace(/_/g, '/');
// Re-pad the string with equals signs to make it a multiple of 4
while (standardBase64.length % 4 !== 0) {
standardBase64 += '=';
}
return standardBase64;
}
Summary and Best Practices
- Never use raw
btoa()andatob()if user-generated content is involved, as you will inevitably crash on an emoji or international character. - Always leverage
TextEncoderandTextDecoderfor robust string handling in the browser. - If you prefer not to write boilerplate code, you can test your strings instantly using our client-side Base64 Decoder tool, which implements all of these UTF-8 and URL-safe best practices under the hood.