JavaScript (ES2026) Module 7 – Strings, Unicode and regular expressions
Text encoding, bytes and Base64
Turn JavaScript strings into UTF-8 bytes and back with TextEncoder and TextDecoder, encode bytes as Base64 or hex, and see why btoa fails on most text.
What you will learn
- Convert between strings and UTF-8 bytes with TextEncoder and TextDecoder
- Encode bytes as Base64, base64url or hex with the Uint8Array methods of ECMAScript 2026
- Explain why btoa fails on text outside Latin-1, and write a fallback that works everywhere
- Decode bytes safely when they are invalid, split into pieces or in an older encoding
Before you start
What you’ll learn · Before you start
What you will learn
- Convert between strings and UTF-8 bytes with TextEncoder and TextDecoder
- Encode bytes as Base64, base64url or hex with the Uint8Array methods of ECMAScript 2026
- Explain why btoa fails on text outside Latin-1, and write a fallback that works everywhere
- Decode bytes safely when they are invalid, split into pieces or in an older encoding
Before you start
On this page
Text is stored and sent as bytes, and a character encoding is the rule that turns characters into bytes and back.
UTF-8 is the encoding of the web, of JSON and of most files, and in JavaScript TextEncoder and TextDecoder convert
between strings and UTF-8 bytes held in a Uint8Array. Base64 solves a different problem: it writes any bytes, an
image or a key or encrypted data, as text made of 64 safe characters, so the bytes can travel through JSON, a URL or
an email. ECMAScript 2026 gives Uint8Array its own toBase64 and fromBase64 methods, with toHex and fromHex
beside them. They replace the old btoa and atob, which accept only characters up to U+00FF and so throw on नमस्ते
and on most of the world’s text. This lesson converts text to bytes and back, takes Base64 apart bit by bit, and ends
with a fallback for runtimes that do not have the new methods yet.
Strings, code units and UTF-8 bytes
A JavaScript string is a sequence of UTF-16 code units, and its length counts them. Most characters are one code
unit; a character above U+FFFF, such as most emoji, is two, a surrogate pair. UTF-8 is a different way to write the same
code points: one to four bytes each, depending on how large the code point is.
| Code points | Bytes in UTF-8, with examples |
|---|---|
| U+0000 to U+007F | 1: the ASCII letters, digits and punctuation |
| U+0080 to U+07FF | 2: é, ß, Greek, Cyrillic, Arabic, Hebrew |
| U+0800 to U+FFFF | 3: Devanagari, Tamil, Chinese, Japanese, currency signs such as € |
| U+10000 to U+10FFFF | 4: most emoji, rarer scripts |
// TextEncoder turns a string into UTF-8 bytes: one to four bytes for each code point.
const encoder = new TextEncoder();
for (const ch of ['A', 'é', '₹', 'न', '😀']) {
const codePoint = `U+${ch.codePointAt(0).toString(16).toUpperCase().padStart(4, '0')}`;
const bytes = encoder.encode(ch);
console.log(`${ch} ${codePoint.padEnd(7)} ${ch.length} code unit(s) ${bytes.length} byte(s): ${bytes.toHex()}`);
}
// A string's length counts UTF-16 code units, not characters and not bytes.
const text = 'Aé😀न';
const bytes = encoder.encode(text);
console.log(`${text}: length ${text.length}, code points ${[...text].length}, UTF-8 bytes ${bytes.length}`);
console.log([...bytes].map((b) => b.toString(16).padStart(2, '0')).join(' '));
// A lone surrogate (half of an emoji) is not a character: it is encoded as U+FFFD, the replacement character.
console.log(encoder.encode('\uD83D').toHex(), '|', '\uD83D'.isWellFormed()); Output
A U+0041 1 code unit(s) 1 byte(s): 41 é U+00E9 1 code unit(s) 2 byte(s): c3a9 ₹ U+20B9 1 code unit(s) 3 byte(s): e282b9 न U+0928 1 code unit(s) 3 byte(s): e0a4a8 😀 U+1F600 2 code unit(s) 4 byte(s): f09f9880 Aé😀न: length 5, code points 4, UTF-8 bytes 10 41 c3 a9 f0 9f 98 80 e0 a4 a8 efbfbd | false
Recorded with Node.js 26.10.0 on macOS 26 arm64. To run it yourself: mise exec node@26.10.0 -- node utf8-bytes.mjs
The same four characters, Aé😀न, are 5 code units, 4 code points and 10 bytes, and each of those numbers matters
somewhere: length for string positions, code points for what a person sees as characters (most of the time), and
bytes for what a database column, an HTTP header or a file actually stores. TextEncoder has no options, because it
only ever produces UTF-8. It also cleans up one problem on the way: a lone surrogate, half of an emoji left over after
a careless slice, is not a character at all, so it becomes the three bytes of U+FFFD, the replacement character.
isWellFormed() tells you beforehand whether a string contains one.
From bytes back to text: TextDecoder
TextDecoder does the opposite, and it is more careful than it looks. Bytes come from outside the program, from a
file, a network response or a device, and they are not always valid UTF-8.
// TextDecoder turns bytes back into a string. 'utf-8' is the default encoding.
const utf8 = new TextDecoder();
console.log(utf8.decode(Uint8Array.fromHex('e0a4a8e0a4ae'))); // नम
// Bytes that are not valid UTF-8 become U+FFFD (�), unless fatal: true asks for an error.
const damaged = Uint8Array.fromHex('4142ff43'); // A, B, an invalid byte, C
console.log(JSON.stringify(utf8.decode(damaged)));
try {
new TextDecoder('utf-8', { fatal: true }).decode(damaged);
} catch (e) {
console.log('fatal:', e.name);
}
// Text read in pieces can split a character. stream: true keeps the unfinished bytes for the next call.
const pieces = [Uint8Array.fromHex('e0a4'), Uint8Array.fromHex('a8e0a4ae')]; // न is cut after its second byte
console.log('piece by piece:', JSON.stringify(pieces.map((p) => new TextDecoder().decode(p)).join('')));
const streaming = new TextDecoder();
console.log('streaming:', JSON.stringify(pieces.map((p, i) => streaming.decode(p, { stream: i < pieces.length - 1 })).join('')));
// TextDecoder also reads older encodings by their labels.
console.log(new TextDecoder('windows-1252').decode(Uint8Array.fromHex('93636166e994'))); // a file saved by old Windows software
console.log(new TextDecoder('utf-16le').decode(Uint8Array.fromHex('28092e09'))); // UTF-16, little-endian Output
नम "AB�C" fatal: TypeError piece by piece: "��म" streaming: "नम" “café” नम
Recorded with Node.js 26.10.0 on macOS 26 arm64. To run it yourself: mise exec node@26.10.0 -- node decode-bytes.mjs
By default an invalid byte becomes U+FFFD, shown as �, so the rest of the text still reads. 0xFF can never appear in
UTF-8, which is why it is replaced. Where a damaged file must be rejected rather than repaired, { fatal: true }
makes decode throw a TypeError instead. Text that arrives in pieces needs { stream: true }: here the bytes of न
are split between two pieces, and decoding each piece on its own turns both halves into replacement characters. A
streaming decoder keeps the unfinished bytes and completes the character with the next piece; the last call, without
stream, flushes it.
TextDecoder also reads the older encodings named in the Encoding Standard, such as windows-1252, shift_jis or
utf-16le, by their labels. The labels have surprises: latin1 and iso-8859-1 both decode as windows-1252,
which gives printable characters such as € and the curly quotes to most of the bytes from 0x80 to 0x9F, where
ISO-8859-1 has control codes. A label the standard does not know throws a RangeError. A UTF-8 byte order
mark at the start is removed, unless you pass ignoreBOM: true.
Base64: bytes as safe text
Many channels carry text but not arbitrary bytes: JSON strings, URLs, HTTP headers, email bodies. Base64 maps every
group of 3 bytes, 24 bits, onto 4 characters of 6 bits each, chosen from an alphabet of 64 plain ASCII characters:
A to Z, a to z, 0 to 9, + and /.
One character, two encodings: UTF-8 bytes, then Base64 text
Text description of the diagram
The character न has the code point U+0928. UTF-8 needs three bytes for it, following the template 1110xxxx 10xxxxxx 10xxxxxx, where the x positions carry the 16 bits of the code point.
A row of 24 bit cells shows the result: 11100000 10100100 10101000. Brackets above the row mark the three bytes, E0 (224), A4 (164) and A8 (168). The cells of the template bits (1110 at the start of the first byte and 10 at the start of the second and third) are grey; the cells that carry the bits of U+0928 are blue.
Brackets below the row cut the same 24 bits into four groups of six: 111000 = 56, 001010 = 10, 010010 = 18 and 101000 = 40. In the Base64 alphabet these are the characters 4, K, S and o. The note at the bottom says that Base64 reads the same 24 bits 6 at a time, so 3 bytes become the 4 characters 4KSo.
// Base64 by hand: every 3 bytes (24 bits) become 4 characters of 6 bits each.
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
function explain(bytes) {
const bits = bytes.map((b) => b.toString(2).padStart(8, '0')).join('');
const sixes = bits.padEnd(Math.ceil(bits.length / 6) * 6, '0').match(/.{6}/g);
const chars = sixes.map((six) => ALPHABET[parseInt(six, 2)]).join('');
return `${bytes.join(' ').padEnd(11)} ${bits.padEnd(24)} → ${sixes.join(' ').padEnd(27)} → ${chars.padEnd(4, '=')}`;
}
console.log(explain([224, 164, 168])); // the three UTF-8 bytes of न
console.log(explain([77, 97, 110])); // 'Man'
console.log(explain([77, 97])); // two bytes: one = of padding
console.log(explain([77])); // one byte: two = of padding
// The size rule: 4 characters for every 3 bytes, rounded up.
const grouped = (n) => String(n).replace(/\B(?=(\d{3})+$)/g, ','); // 1000000 → 1,000,000
for (const n of [1, 2, 3, 10, 1_000_000]) {
console.log(`${grouped(n)} ${n === 1 ? "byte" : "bytes"} → ${grouped(4 * Math.ceil(n / 3))} characters`);
} Output
224 164 168 111000001010010010101000 → 111000 001010 010010 101000 → 4KSo 77 97 110 010011010110000101101110 → 010011 010110 000101 101110 → TWFu 77 97 0100110101100001 → 010011 010110 000100 → TWE= 77 01001101 → 010011 010000 → TQ== 1 byte → 4 characters 2 bytes → 4 characters 3 bytes → 4 characters 10 bytes → 16 characters 1,000,000 bytes → 1,333,336 characters
Recorded with Node.js 26.10.0 on macOS 26 arm64. To run it yourself: mise exec node@26.10.0 -- node base64-by-hand.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
When the number of bytes is not a multiple of three, the last group is padded with zero bits, and = characters
stand in for the missing characters: one = after two bytes, two after one byte. That is why every Base64 text of
standard form has a length divisible by four, and why encoding makes data about a third larger: 1,000,000 bytes
become 1,333,336 characters. Base64 is an encoding, not encryption. Anyone can decode it, so it hides nothing, and a
password or token written in Base64 is as exposed as the plain text.
The variant called base64url replaces + and / with - and _, because + and / mean something in a URL, and
usually leaves out the = padding, which a URL would have to escape. RFC 4648 defines both alphabets, and the three
parts of a JSON Web Token are base64url without padding, as RFC 7515 specifies.
Uint8Array.toBase64 and fromBase64
ECMAScript 2026 adds four methods to Uint8Array: toBase64() and toHex() on an array, and the static
Uint8Array.fromBase64() and Uint8Array.fromHex() that build a new one.
// ES2026: Uint8Array has Base64 and hex methods of its own.
const text = 'नमस्ते';
const bytes = new TextEncoder().encode(text);
const b64 = bytes.toBase64();
console.log(`${text} → ${bytes.length} bytes → ${b64}`);
const back = new TextDecoder().decode(Uint8Array.fromBase64(b64));
console.log(`${b64} → ${back} (same text: ${back === text})`);
// The URL-safe alphabet uses - and _ instead of + and /; tokens and URLs usually leave out the = padding.
const id = new Uint8Array([251, 255, 191, 251, 255]);
console.log(id.toBase64());
console.log(id.toBase64({ alphabet: 'base64url' }));
console.log(id.toBase64({ alphabet: 'base64url', omitPadding: true }));
console.log([...Uint8Array.fromBase64('-_-_-_8', { alphabet: 'base64url' })].join(' '));
// Hex works the same way.
console.log(bytes.toHex(), [...Uint8Array.fromHex('cafe')].join(' '));
// Invalid input throws a SyntaxError instead of guessing.
for (const bad of ['-_8', 'abc$', 'TWFuA']) {
try {
Uint8Array.fromBase64(bad);
} catch (e) {
console.log(`${JSON.stringify(bad)}: ${e.name}`);
}
} Output
नमस्ते → 18 bytes → 4KSo4KSu4KS44KWN4KSk4KWH 4KSo4KSu4KS44KWN4KSk4KWH → नमस्ते (same text: true) +/+/+/8= -_-_-_8= -_-_-_8 251 255 191 251 255 e0a4a8e0a4aee0a4b8e0a58de0a4a4e0a587 202 254 "-_8": SyntaxError "abc$": SyntaxError "TWFuA": SyntaxError
Recorded with Node.js 26.10.0 on macOS 26 arm64. To run it yourself: mise exec node@26.10.0 -- node base64-roundtrip.mjs
The text goes through UTF-8 first: TextEncoder makes the 18 bytes of नमस्ते, and toBase64 encodes the bytes, never
the string itself. The options cover the common variants. alphabet: 'base64url' switches to - and _, and
omitPadding: true drops the = signs. On the way back, fromBase64 takes the same alphabet option, and
lastChunkHandling decides what happens with an incomplete last group: 'loose', the default, accepts it with or
without padding, 'strict' demands correct padding and zero bits, and 'stop-before-partial' decodes only the
complete groups. Spaces and line breaks in the input are skipped. Anything else that is not valid input throws a
SyntaxError rather than producing garbage: here, characters of the other alphabet, a stray $ and a single
character left over at the end. To fill an existing
array instead of creating one, setFromBase64 and setFromHex write into it and return how many characters they read
and how many bytes they wrote.
Why btoa fails on most text
btoa and atob date from the early browsers, and they work on “binary strings”: strings in which every character
stands for one byte and therefore must be between U+0000 and U+00FF. The HTML standard specifies that btoa throws an
InvalidCharacterError for anything else.
// btoa and atob work on "binary strings": every character must be below U+0100 and stands for one byte.
console.log(btoa('hello'), atob('aGVsbG8='));
try {
btoa('नमस्ते');
} catch (e) {
console.log(e.name); // न is U+0928: it is not a byte
}
// Worse, text with characters below U+0100 is accepted and encoded as Latin-1, not UTF-8.
console.log(`btoa('café') = ${btoa('café')}, the UTF-8 bytes of café = ${new TextEncoder().encode('café').toBase64()}`);
// The fallback for runtimes without toBase64: UTF-8 bytes → one character per byte → btoa.
function toBase64Fallback(bytes) {
let binary = '';
for (const b of bytes) binary += String.fromCharCode(b); // a loop, because spreading a large array overflows the call stack
return btoa(binary);
}
function fromBase64Fallback(b64) {
const binary = atob(b64);
return Uint8Array.from(binary, (ch) => ch.charCodeAt(0));
}
const bytes = new TextEncoder().encode('नमस्ते');
console.log(toBase64Fallback(bytes), toBase64Fallback(bytes) === bytes.toBase64());
console.log(new TextDecoder().decode(fromBase64Fallback('4KSo4KSu4KS44KWN4KSk4KWH'))); Output
aGVsbG8= hello
InvalidCharacterError
btoa('café') = Y2Fm6Q==, the UTF-8 bytes of café = Y2Fmw6k=
4KSo4KSu4KS44KWN4KSk4KWH true
नमस्ते
Recorded with Node.js 26.10.0 on macOS 26 arm64. To run it yourself: mise exec node@26.10.0 -- node btoa-trap.mjs
btoa('नमस्ते') throws, because न is U+0928, far beyond one byte. The quieter failure is worse: btoa('café') works,
but it encodes é as the single Latin-1 byte E9, so the result, Y2Fm6Q==, is not the Base64 of the UTF-8 text a
server expects, Y2Fmw6k=. The cure in both cases is the same: encode the text to UTF-8 bytes first, then encode the
bytes. Where toBase64 is missing, the bytes can be turned into a binary string, one character per byte, and passed
to btoa; atob and charCodeAt reverse it. Build the binary string in a loop: String.fromCharCode(...bytes)
looks shorter, but it passes every byte as a separate argument and throws a RangeError for large arrays; in Node.js
26 it already fails at 200,000 bytes. Node.js marks btoa and atob as legacy and recommends Buffer in Node code: Buffer.from(bytes)
has toString('base64') and toString('base64url'), and Buffer.from(text, 'base64') decodes.
Where the new methods work
toBase64, fromBase64, toHex, fromHex and the two setFrom… methods are part of ECMAScript 2026. The
compatibility data of MDN Web Docs lists them from Chrome 140, Firefox 133, Safari 18.2 and Node.js 25. Node.js 24
does not have them, so every example on this page ran on Node.js 26.10.0. The QuickJS runner of the Run
buttons has neither these methods nor TextEncoder, TextDecoder, btoa and atob, so only the hand-made
Base64 example can run in the page; the others show what Node.js printed. Code that must run everywhere checks
first: typeof Uint8Array.prototype.toBase64 === 'function'.
Exercise
Exercise · Medium · JavaScript
Encode and decode bytes as base64url, with a fallback
Write two functions in base64url.mjs, the encoding that tokens and URLs use: the Base64 alphabet with - and _ in place of + and /, and no = padding.
toBase64Url(bytes)takes aUint8Arrayand returns its base64url text without padding:new Uint8Array([251, 255])gives'-_8', the bytes ofMangive'TWFu', and an empty array gives''.fromBase64Url(text)turns such text back into aUint8Array. It accepts the text with or without=padding at the end. It throws aSyntaxErrorfor a character outside the alphabet (+,/,*, a space …), for=anywhere but at the end, and for a length that leaves a single character over ('TWFuA').
Use bytes.toBase64() and Uint8Array.fromBase64() when they exist, and your own conversion when they do not. The Run button of this page uses a small JavaScript engine that has neither the new Base64 methods nor btoa and atob, so the conversion you write yourself is what the tests check here.
Starter code · base64url.mjs
/** The bytes as base64url text (alphabet with - and _, no = padding). */
export function toBase64Url(bytes) {
// Replace this with your code.
return '';
}
/** The bytes of base64url text, with or without = padding; a SyntaxError for anything else. */
export function fromBase64Url(text) {
// Replace this with your code.
return new Uint8Array(0);
} The sample tests · base64url.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { fromBase64Url, toBase64Url } from './base64url.mjs';
const bytesOf = (...values) => new Uint8Array(values);
test('toBase64Url uses - and _ and leaves out the padding', () => {
assert.equal(toBase64Url(bytesOf(251, 255)), '-_8');
assert.equal(toBase64Url(bytesOf(251, 255, 191)), '-_-_');
});
test('toBase64Url: groups of three bytes, and the shorter last groups', () => {
assert.equal(toBase64Url(bytesOf(77, 97, 110)), 'TWFu');
assert.equal(toBase64Url(bytesOf(77, 97)), 'TWE');
assert.equal(toBase64Url(bytesOf(77)), 'TQ');
assert.equal(toBase64Url(bytesOf()), '');
assert.equal(toBase64Url(bytesOf(224, 164, 168)), '4KSo');
});
test('fromBase64Url reads text with and without padding', () => {
assert.deepEqual([...fromBase64Url('-_8')], [251, 255]);
assert.deepEqual([...fromBase64Url('TWE')], [77, 97]);
assert.deepEqual([...fromBase64Url('TWE=')], [77, 97]);
assert.deepEqual([...fromBase64Url('')], []);
assert.ok(fromBase64Url('TWFu') instanceof Uint8Array, 'returns a Uint8Array');
});
test('every byte value survives the round trip', () => {
const all = Uint8Array.from({ length: 256 }, (_, i) => i);
assert.deepEqual([...fromBase64Url(toBase64Url(all))], [...all]);
});
test('fromBase64Url throws a SyntaxError for characters outside the alphabet and impossible lengths', () => {
for (const bad of ['+/8', 'TW E', 'TW*u', 'TWFuA']) assert.throws(() => fromBase64Url(bad), SyntaxError, JSON.stringify(bad));
}); A hint
Work in groups of three bytes. (b0 << 16) | (b1 << 8) | b2 puts them into one 24-bit number, and (n >> 18) & 63, (n >> 12) & 63, (n >> 6) & 63 and n & 63 are the four 6-bit indices into 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'. A last group of one byte gives two characters, and a group of two bytes gives three.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
Why does btoa(‘नमस्ते’) throw, and how do you Base64-encode any text correctly? btoa encodes a “binary
string”, in which every character stands for one byte, so it accepts only characters from U+0000 to U+00FF and throws
an InvalidCharacterError for anything else; न is U+0928. Base64 encodes bytes, not characters, so the text has to be
turned into bytes first, almost always UTF-8, with new TextEncoder().encode(text). The bytes then go to
bytes.toBase64() where ECMAScript 2026 is available, or, as a fallback, into a binary string built one byte per
character in a loop and passed to btoa. Decoding reverses the steps: Uint8Array.fromBase64 or atob gives the
bytes, and new TextDecoder().decode(bytes) the text. Calling btoa directly on text with é in it does not throw,
but it silently produces the Base64 of Latin-1 bytes rather than of UTF-8.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
To greet the user, a web page decodes the payload of a JWT with JSON.parse(atob(token.split('.')[1])). It works in testing, but for some users it throws, and for others names such as José come out garbled. Explain both failures, write a correct decoder, and say what the page must never do with the result.
-
Mid to senior
A mobile web app uploads photos as Base64 strings inside a JSON body. The API gateway rejects bodies over 4 MB, photos of about 3 MB already fail, and users on low-end phones report that the page freezes. Explain the size and memory costs with numbers, and propose a better upload.
Key takeaways
- A string’s
lengthcounts UTF-16 code units; UTF-8 needs 1 to 4 bytes per code point, so नमस्ते is 6 code units and 18 bytes. new TextEncoder().encode(text)always gives UTF-8 bytes;new TextDecoder(label, options).decode(bytes)reads UTF-8 or an older encoding, withfatalto reject bad bytes andstreamfor text in pieces.- Base64 writes 3 bytes as 4 characters from a 64-character alphabet, with
=padding; it is about a third larger and it is not encryption. - base64url uses
-and_instead of+and/and usually has no padding. - In ECMAScript 2026,
bytes.toBase64(),Uint8Array.fromBase64(),toHex()andfromHex()do the work, with options for the alphabet, the padding and the last group, and aSyntaxErrorfor bad input. btoaandatobonly handle characters up to U+00FF: encode text to UTF-8 bytes first, and keep them for old runtimes only.
References
- ECMAScript 2026 Language Specification: Uint8Array.prototype.toBase64 (Ecma International)
- ECMAScript 2026 Language Specification: Uint8Array.fromBase64 (Ecma International)
- ECMAScript 2026 Language Specification: Uint8Array.prototype.toHex (Ecma International)
- ECMAScript 2026 Language Specification: String.prototype.isWellFormed (Ecma International)
- Encoding Standard: Interface TextEncoder (WHATWG)
- Encoding Standard: Interface TextDecoder (WHATWG)
- Encoding Standard: Names and labels (WHATWG)
- HTML Living Standard: Base64 utility methods (WHATWG)
- RFC 4648: The Base16, Base32, and Base64 Data Encodings (IETF)
- RFC 7515: JSON Web Signature (JWS), section 2: Base64url Encoding (IETF)
- RFC 3629: UTF-8, a transformation format of ISO 10646 (IETF)
- RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format, section 8.1: Character Encoding (IETF)
- Uint8Array to/from base64 (proposal) (Ecma International (TC39))
- Uint8Array.prototype.toBase64() (MDN Web Docs)
- Node.js v24 documentation: buffer.atob() and buffer.btoa() (OpenJS Foundation (Node.js))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress