How to Decode Base64 in JavaScript (Browser & Node.js)

Published: 2025-11-19 | Category: Developer

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