Upload a file
  1. base64hub
  2. Learn Base64
  3. Uint8Array.toBase64()
Guide

Uint8Array.toBase64 and fromBase64 in JavaScript

JavaScript finally has a Base64 API that works on bytes, handles Base64URL and lets you choose how strict decoding is. It replaces most uses of btoa, atob and hand-written loops.

Encoding and decoding in one line

The API adds three methods for Base64 and three for hex:

MethodWhat it does
bytes.toBase64(options)Uint8Array to a Base64 string
Uint8Array.fromBase64(string, options)Base64 string to a new Uint8Array
bytes.setFromBase64(string, options)Decode into an existing Uint8Array
bytes.toHex()Uint8Array to a lowercase hex string
Uint8Array.fromHex(string)Hex string to a new Uint8Array
bytes.setFromHex(string)Decode hex into an existing Uint8Array

Text has to become bytes first. TextEncoder always produces UTF-8, so Unicode works without tricks:

const bytes = new TextEncoder().encode("Grüße, 世界");
const b64 = bytes.toBase64();
console.log(b64); // "R3LDvMOfZSwg5LiW55WM"

const back = new TextDecoder().decode(Uint8Array.fromBase64(b64));
console.log(back); // "Grüße, 世界"

An ArrayBuffer, for example from fetch().arrayBuffer() or crypto.subtle.digest(), needs a view: new Uint8Array(buffer).toBase64(). Other typed arrays do not have the methods at all. To encode the raw memory of a Uint16Array, wrap it: new Uint8Array(u16.buffer, u16.byteOffset, u16.byteLength).

Options: alphabet, omitPadding, lastChunkHandling

alphabet

"base64" (the default) uses + and /. "base64url" uses - and _. The option applies to both encoding and decoding, and a decoder set to one alphabet rejects the other alphabet’s characters.

const data = new Uint8Array([251, 255]);
data.toBase64();                                         // "+/8="
data.toBase64({ alphabet: "base64url" });                // "-_8="
data.toBase64({ alphabet: "base64url", omitPadding: true }); // "-_8"

Uint8Array.fromBase64("-_8", { alphabet: "base64url" }); // Uint8Array [251, 255]
Uint8Array.fromBase64("-_8");                            // SyntaxError

omitPadding

toBase64 adds = padding by default, even with the Base64URL alphabet. Pass omitPadding: true for JWT-style output. For decoding, padding is controlled by lastChunkHandling.

lastChunkHandling

This option decides what fromBase64 and setFromBase64 do with the final group of characters. The three modes, tested with input that decodes to the bytes of Hi?>:

Input"loose" (default)"strict""stop-before-partial"
SGk/Pg==Hi?>Hi?>Hi?>
SGk/Pg (no padding)Hi?>SyntaxErrorHi? (stops before Pg)
SGk/Ph== (non-zero padding bits)Hi?>SyntaxErrorHi?>
SGk/P (one leftover character)SyntaxErrorSyntaxErrorHi?
  • loose behaves like atob: padding is optional, and leftover bits in the last character are ignored.
  • strict accepts only canonical, padded input, as RFC 4648 describes it. Use it when the encoded form must be unique, for example when Base64 strings are compared or signed.
  • stop-before-partial decodes all complete 4-character groups and leaves an incomplete tail alone. That is meant for streaming, where the rest of the group arrives in the next chunk.

In every mode, ASCII whitespace (spaces, tabs, newlines) is skipped, so MIME-wrapped input decodes directly. Any other character outside the alphabet throws a SyntaxError.

Writing into an existing buffer

setFromBase64 and setFromHex decode into a Uint8Array you already have and return how much they used:

const target = new Uint8Array(8);
const result = target.setFromBase64("SGVsbG8gd29ybGQ=");
console.log(result); // { read: 8, written: 6 }

Hello world is 11 bytes and the target holds 8. The decoder only writes whole 3-byte groups that fit, so it stops after 6 bytes and reports that it read 8 characters. Call it again with the rest of the string and a subarray of the target to continue. Combined with stop-before-partial, this lets you decode a stream chunk by chunk into a preallocated buffer:

const out = new Uint8Array(1024);
const { read, written } = out.setFromBase64("SGk/Pg", {
  lastChunkHandling: "stop-before-partial",
});
// read: 4, written: 3. Keep "Pg" and prepend it to the next chunk.

The hex methods follow the same pattern. toHex() always writes lowercase, fromHex() accepts both cases, and an odd number of digits is a SyntaxError:

new Uint8Array([0xde, 0xad, 0xbe, 0xef]).toHex(); // "deadbeef"
Uint8Array.fromHex("CAFE");                       // Uint8Array [202, 254]

Why btoa and atob are awkward

btoa and atob date from the early days of browsers and work on “binary strings”: JavaScript strings where every character code is a byte value from 0 to 255. That causes three problems.

Unicode either throws or silently goes wrong. Characters above U+00FF make btoa throw an InvalidCharacterError (Chrome: “The string to be encoded contains characters outside of the Latin1 range”). Characters between U+0080 and U+00FF are worse: they encode without an error, as Latin-1. btoa("Grüße") returns R3L832U=, while the UTF-8 encoding that every other system expects is R3LDvMOfZQ==.

Decoding returns a string, not bytes. atob("Q2Fmw6k=") returns "Café". To get real text you convert the string to bytes and then decode them as UTF-8, which takes a loop or Uint8Array.from(atob(s), c => c.charCodeAt(0)).

Large inputs need care. The common one-liner btoa(String.fromCharCode(...bytes)) passes every byte as a function argument, and with a 1 MB array it throws RangeError: Maximum call stack size exceeded. You have to build the string in chunks.

There is also no Base64URL support and no control over padding or strictness. The old workaround btoa(unescape(encodeURIComponent(text))) does produce UTF-8 Base64, but relies on the deprecated unescape. With the new API, all of this becomes new TextEncoder().encode(text).toBase64().

Browser and Node.js support

The methods come from the TC39 proposal “Uint8Array to/from Base64”, which reached Stage 4 in July 2025 and is part of ECMAScript 2026.

RuntimeFirst version with all six methods
Chrome, Edge140
Firefox133
Safari (macOS and iOS)18.2
Node.js25.0.0

With Chrome 140 the feature became Baseline in September 2025, so every current major browser has it. In Node.js we tested the methods directly. Node 25.0.0 and 26.12.1 have all six. Node 24.22.1, an LTS release, has none of them by default. Its V8 accepts an experimental --js-base-64 flag, but in our test that flag exposed toBase64, fromBase64, toHex and fromHex without setFromBase64 or setFromHex, so treat Node 24 as unsupported. On Node 24 and older, use Buffer.from(b64, "base64") and buf.toString("base64"), which also accept "base64url". Deno 2.5 and Bun 1.1.22 support the API too, according to MDN’s compatibility data.

A fallback for older runtimes

Feature-detect and fall back to btoa/atob with chunking. This version was tested on Node 24 (fallback path) and Node 26 (native path) with UTF-8 text and a 1 MB array:

function bytesToBase64(bytes) {
  if (typeof bytes.toBase64 === "function") return bytes.toBase64();
  let bin = "";
  for (let i = 0; i < bytes.length; i += 0x8000) {
    bin += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
  }
  return btoa(bin);
}

function base64ToBytes(b64) {
  if (typeof Uint8Array.fromBase64 === "function") return Uint8Array.fromBase64(b64);
  return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
}

The fallback covers the default options only. If you need base64url, strict or setFromBase64 everywhere, use a full polyfill: core-js ships one as es.uint8-array.to-base64, es.uint8-array.from-base64 and related modules.

To try encodings without writing code, use the Base64 encoder and decoder, or Base64URL for the URL-safe alphabet. More JavaScript examples, including Node’s Buffer, are in Base64 in JavaScript and Base64 in Node.js.

FAQ

Is the new Base64 API safe to ship today?

In browsers, yes: it ships in Chrome and Edge 140, Firefox 133 and Safari 18.2. In Node.js it needs version 25 or later. Keep a fallback if you support older browsers or Node 24 and below.

Does Uint8Array.fromBase64 accept Base64URL?

Yes, with { alphabet: "base64url" }. Without that option, - and _ cause a SyntaxError. Padding is optional in the default loose mode for both alphabets.

How do I encode a string with toBase64?

Turn it into UTF-8 bytes first: new TextEncoder().encode(text).toBase64(). To decode, reverse it with new TextDecoder().decode(Uint8Array.fromBase64(b64)).

Why is toBase64 not a function on my Buffer or Uint16Array?

The methods exist only on Uint8Array and its subclasses. Node’s Buffer gets them from Node 25 on. For other typed arrays, wrap the memory: new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength).

Keep reading