How to Base64 Encode and Decode UTF-8 Strings in JavaScript Without atob and btoa Encoding Errors
Fix the classic JavaScript 'Character Out of Range' atob and btoa error. Learn modern TextEncoder and TextDecoder techniques for international UTF-8 Base64 strings.

Every JavaScript developer who has worked with browser Base64 encoding has encountered the infamous runtime exception: "Uncaught DOMException: Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range."
This error occurs because the browser's native btoa() (Binary to ASCII) function was designed in the late 1990s. It strictly accepts 8-bit Latin1 characters (code points 0 to 255). The moment your user's input contains an emoji (🎉), accented letters (é, ñ), or international scripts (Arabic, Chinese, Urdu), btoa() crashes immediately.
This guide explains the underlying Unicode multi-byte encoding problem, demonstrates modern TextEncoder and TextDecoder patterns, and provides safe helper functions for full-stack JavaScript. You can encode and decode UTF-8 Base64 payloads privately using Synctoolo's free Base64 Encoder / Decoder.
Why atob() and btoa() Break on Unicode
In standard ASCII and Latin-1, every character occupies exactly 1 byte (8 bits). However, the modern web is built on UTF-8. In UTF-8, characters are variable-length:
- Standard English letters (A-Z): 1 byte
- Accented Latin & Greek characters: 2 bytes
- Asian characters (Kanji, Devanagari, Arabic): 3 bytes
- Emojis and complex glyphs: 4 bytes
When you pass a 4-byte emoji like 🚀 into btoa(), the browser sees a code point value of U+1F680 (decimal 128,640). Because this exceeds 255, btoa() throws an exception.
The Modern Solution: TextEncoder and Uint8Array
The modern, standard-compliant way to encode UTF-8 text to Base64 in all evergreen browsers and Node.js uses TextEncoder:
// 1. Safe UTF-8 to Base64 Encoding
function utf8ToBase64(str) {
const bytes = new TextEncoder().encode(str);
const binaryString = Array.from(bytes, (byte) => String.fromCharCode(byte)).join("");
return btoa(binaryString);
}
// 2. Safe Base64 to UTF-8 Decoding
function base64ToUtf8(base64) {
const binaryString = atob(base64);
const bytes = Uint8Array.from(binaryString, (char) => char.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
// Test with emojis and international scripts:
const sample = "Synctoolo: 🚀 Free Tools for Devs & Creators (مرحبا / 你好)";
const encoded = utf8ToBase64(sample);
const decoded = base64ToUtf8(encoded);
console.log(decoded === sample); // Returns TRUE!
Why the Legacy escape() Hack Is Deprecated
Older StackOverflow answers frequently recommend this hack: btoa(unescape(encodeURIComponent(str))). While this trick worked in legacy browsers, escape() and unescape() were officially deprecated in ECMAScript 5 and removed from modern web standards. Relying on deprecated functions risks unexpected failures in strict environments and modern edge runtimes.
Encode, decode, and verify international UTF-8 Base64 payloads directly in your browser with Synctoolo's free Base64 Encoder / Decoder.
Base64 URL-Safe Encoding (RFC 4648 Section 5)
Standard Base64 encoding utilizes the 64-character set A-Z, a-z, 0-9, +, and /, with = for padding. When transmitting Base64 encoded strings within HTTP URL query parameters, URL paths, or JSON Web Tokens (JWTs), standard Base64 creates major routing errors:
- The plus character (
+) is interpreted by web servers as an encoded whitespace character (space). - The slash character (
/) is parsed as an HTTP URL path segment separator. - The equal sign (
=) is parsed as a query parameter key-value delimiter.
To eliminate these issues, Base64URL (RFC 4648 § 5) modifies the alphabet:
// Convert Standard Base64 to Base64URL
function toBase64Url(base64) {
return base64
.replace(/\+/g, "-") // Replace + with -
.replace(/\//g, "_") // Replace / with _
.replace(/=+$/, ""); // Strip trailing padding =
}
// Convert Base64URL back to Standard Base64
function fromBase64Url(base64Url) {
let base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
while (base64.length % 4 !== 0) {
base64 += "=";
}
return base64;
}
Universal Cross-Platform UTF-8 Helper (Browser & Node.js)
Modern applications require code that runs seamlessly in both browser client components and Node.js server runtimes. Here is an production-ready isomorphic implementation:
// Isomorphic UTF-8 Base64 utility
export const Base64Util = {
encode(text) {
if (typeof Buffer !== "undefined") {
return Buffer.from(text, "utf-8").toString("base64");
}
const bytes = new TextEncoder().encode(text);
const binString = Array.from(bytes, (byte) => String.fromCharCode(byte)).join("");
return btoa(binString);
},
decode(base64) {
if (typeof Buffer !== "undefined") {
return Buffer.from(base64, "base64").toString("utf-8");
}
const binString = atob(base64);
const bytes = Uint8Array.from(binString, (m) => m.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
};
This implementation handles multi-byte Unicode strings (including Chinese, Arabic, Cyrillic, and emojis) without throwing DOMException or corrupting character byte boundaries.
Tools mentioned in this article
FAQ
What does the name btoa stand for?+
The acronym btoa stands for 'Binary to ASCII'. Conversely, atob stands for 'ASCII to Binary'.
How does Node.js handle UTF-8 Base64?+
In Node.js, the Buffer class handles UTF-8 Base64 natively without helper functions: Buffer.from(str, 'utf-8').toString('base64') and Buffer.from(b64, 'base64').toString('utf-8').
We build and review free, privacy-first tools at Synctoolo.
Keep reading

Compare JSON, YAML, and TOML data serialization formats. Learn parsing speed benchmarks, indentation pitfalls, security risks, and best use cases for 2026.

Learn how to validate email addresses with regular expressions. Understand the official RFC 5322 standard, catastrophic backtracking risks, and practical regex patterns.