· 5 min read
Uint8Array toBase64 and fromBase64: native base64 in JavaScript
Uint8Array.toBase64() and fromBase64() encode and decode bytes natively, with hex, base64url and buffer options. How they work and where they run.

Uint8Array.toBase64() and Uint8Array.fromBase64() are built-in JavaScript methods that convert bytes to and from base64 without btoa, atob or a library. They also come with hex versions, toHex() and fromHex(), plus setFromBase64() and setFromHex() for writing into an existing buffer. They became Baseline in September 2025, so current Chrome, Edge, Firefox and Safari all have them, and so does Node.js 25.
If your codebase still has a helper that loops over bytes with String.fromCharCode just to call btoa, this is the standard replacement.
What problem do the Uint8Array base64 methods solve?
Base64 is an encoding that represents arbitrary bytes using 64 printable characters, so binary data can travel through text-only channels like JSON, URLs, HTTP headers and data URLs.
The browser has had btoa() and atob() for a long time, but they work on strings, not bytes. btoa expects a "binary string" where each character stands for one byte. Pass it real text with characters outside Latin-1 and it throws:
btoa("héllo ✓"); // InvalidCharacterError
So the usual workaround was to encode the text to UTF-8 bytes, turn every byte back into a one-character string, join them and then call btoa. Going the other way meant atob followed by a loop through charCodeAt. It worked, but it was slow for large inputs, easy to get wrong and had no support for the URL-safe alphabet.
In Node.js you could use Buffer.from(bytes).toString("base64"), but that is not available in browsers, Deno's web APIs or edge runtimes that avoid Node globals. The new methods give every environment the same API.
How do toBase64 and fromBase64 work?
Encoding is a method on any Uint8Array. Decoding is a static method that returns a new Uint8Array.
const bytes = new TextEncoder().encode("hello, world");
bytes.toBase64(); // "aGVsbG8sIHdvcmxk"
bytes.toHex(); // "68656c6c6f2c20776f726c64"
const decoded = Uint8Array.fromBase64("aGVsbG8sIHdvcmxk");
new TextDecoder().decode(decoded); // "hello, world"
Uint8Array.fromHex("cafe"); // Uint8Array(2) [202, 254]
Because a Buffer in Node.js is a subclass of Uint8Array, the methods work on buffers too. A subarray() view encodes only the bytes in that view, which is handy when you want to encode a slice without copying it.
Text still needs TextEncoder and TextDecoder on either side. The base64 methods only deal with bytes, which is exactly why they don't have the Unicode problem that btoa has.
What options do the base64 methods take?
Both directions take an optional options object.
| Option | Used by | Values | Default |
|---|---|---|---|
alphabet | toBase64, fromBase64, setFromBase64 | "base64" or "base64url" | "base64" |
omitPadding | toBase64 | true or false | false |
lastChunkHandling | fromBase64, setFromBase64 | "loose", "strict", "stop-before-partial" | "loose" |
The base64url alphabet swaps + and / for - and _, so the output can sit in a URL or file name without escaping. It is the form used in JWTs and many token formats, which usually drop the trailing = as well:
const b = new Uint8Array([251, 255, 191, 104, 105]);
b.toBase64(); // "+/+/aGk="
b.toBase64({ alphabet: "base64url" }); // "-_-_aGk="
b.toBase64({ alphabet: "base64url", omitPadding: true }); // "-_-_aGk"
When decoding, the alphabet must match. Decoding "-_-_aGk" with the default alphabet throws a SyntaxError, because - and _ are not standard base64 characters.
lastChunkHandling controls what happens with the final group of characters. Base64 works in chunks of four characters, and the last chunk may be shorter or padded:
"loose"accepts a final chunk of two or three characters with or without padding."aGk"decodes to two bytes."strict"requires the final chunk to be fully padded and rejects stray bits."aGk"throws."stop-before-partial"decodes every complete chunk and quietly leaves an incomplete one at the end. This is meant for streaming, where the rest of the chunk arrives in the next piece of input.
ASCII whitespace in the input, including spaces and newlines, is ignored, so base64 copied from a wrapped PEM file or an email body decodes without cleanup.
How do you decode into an existing buffer?
setFromBase64() and setFromHex() write into an array you already have, starting at index 0, instead of allocating a new one. They return an object with two counts: read, the number of input characters consumed, and written, the number of bytes written.
const buf = new Uint8Array(8);
buf.setFromBase64("aGVsbG8sIHdvcmxk");
// { read: 8, written: 6 }
new Uint8Array(4).setFromHex("deadbeef00");
// { read: 8, written: 4 }
If the target is too small, decoding stops at the last complete chunk that fits. In the first example, the next chunk would produce three more bytes and only two slots are left, so it stops after six.
Use read to know where to resume. Together with "stop-before-partial", this lets you decode a large base64 stream into a fixed, reused buffer without building one huge string or array.
Where can you use them today?
| Environment | Support |
|---|---|
| Chrome and Edge | 140 and later |
| Firefox | 133 and later |
| Safari | 18.2 and later, including iOS |
| Node.js | 25; not available in Node.js 24 |
Node.js 24 is still a common LTS target, and there these methods are simply undefined. If you ship code that runs on older runtimes, feature-detect and fall back:
const toB64 = (bytes) =>
typeof bytes.toBase64 === "function"
? bytes.toBase64()
: Buffer.from(bytes).toString("base64");
In TypeScript, the types come from the newer lib settings. If the compiler says toBase64 does not exist on Uint8Array, update TypeScript and include a recent esnext lib, or add a small declaration until your setup catches up.
A few other things are worth knowing:
- Errors are thrown, not ignored. Bad characters, an odd-length hex string or a final chunk that fails the
lastChunkHandlingrule throw aSyntaxError. Wrong argument types throw aTypeError. Wrap decoding of user input intry/catch. - Hex output is lowercase.
fromHexaccepts both cases, buttoHexalways produces lowercase. - Only
Uint8Arrayhas them. For aDataViewor another typed array, make aUint8Arrayview over the sameArrayBufferfirst.
Key takeaways
bytes.toBase64()andUint8Array.fromBase64(str)replace thebtoa/atobbyte-string dance.toHex()andfromHex()do the same for hex, andsetFromBase64()andsetFromHex()decode into an existing buffer.- Use
alphabet: "base64url"andomitPadding: truefor URL-safe tokens. - They are Baseline since September 2025 and available in Node.js 25, but not in Node.js 24.
- Invalid input throws a
SyntaxError, so handle errors when decoding anything you don't control.
FAQ
Is Uint8Array.toBase64 faster than btoa?
It avoids building an intermediate string with one character per byte, which is the expensive part of the old approach. Exact speed depends on the engine and input size, so measure if it matters for your workload.
Can I encode a string directly with toBase64?
No. Convert the string to bytes with new TextEncoder().encode(str) first, then call toBase64(). That route handles any Unicode text correctly.
Should I stop using Buffer for base64 in Node.js?
Not necessarily. Buffer works fine in Node.js. The new methods matter when you want the same code to run in browsers, Deno, workers and Node.js without depending on Node globals.