btoa() and atob() handle only "binary strings", so they throw on text outside Latin-1. ES2026 adds direct conversions (Baseline 2025; also in Node.js 25.8 2,131 ): toBase64({ alphabet, omitPadding }), Uint8Array.fromBase64(), setFromBase64() into an existing array, and toHex(), Uint8Array.fromHex() and setFromHex(). The 'base64url' alphabet uses - and _ as in URLs and JWTs.
const bytes = new TextEncoder().encode('Hi? ✓'); // UTF-8 bytes
console.log(bytes.toBase64(), bytes.toBase64({ alphabet: 'base64url', omitPadding: true }));
console.log(bytes.toHex(), new TextDecoder().decode(Uint8Array.fromHex('4869')));
const target = new Uint8Array(4); // preallocated
console.log(target.setFromBase64('SGk/IOKckw=='), target);SGk/IOKckw== SGk_IOKckw
48693f20e29c93 Hi
{ read: 4, written: 3 } Uint8Array(4) [ 72, 105, 63, 0 ]setFromBase64() stops at the last whole chunk that fits and reports read so you can resume. Invalid input throws a SyntaxError. For older browsers, feature-detect and load the core-js 25,536 polyfill (Polyfills).