util.TextEncoder
Encodes a JavaScript string into a Uint8Array of UTF-8 bytes.
new util.TextEncoder()- Added in Node.js v8.3.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$encode.inputoptional | string= '' (an empty string) | Passed to textEncoder.encode([input]): the text to encode. Returns a Uint8Array of UTF-8 bytes. |
$encodeInto.src | string | Passed to textEncoder.encodeInto(src, dest): the text to encode. |
$encodeInto.dest | Uint8Array | Passed to textEncoder.encodeInto(src, dest): the array to hold the encoded result. Returns { read, written }. |
Return value
TextEncoderA TextEncoder instance with encode(), encodeInto() and a read-only encoding property.
Errors
The constructor takes no arguments and does not throw; encodeInto() throws if `dest` is not a Uint8Array.
Changelog
- PHP v12.11.0textEncoder.encodeInto(src, dest) was added, letting you encode into a buffer you already own.
- PHP v11.0.0The class became available on the global object, so `new TextEncoder()` works without importing node:util.
util.TextEncoder is Node's implementation of the WHATWG Encoding Standard class of the same name. It is exposed as a global too, so new TextEncoder() works with no import — the util export exists mainly for explicitness and for Node versions before v11.
When to reach for it
Reach for it when the code must also run in a browser, a Deno runtime, or a Cloudflare Worker, or when an API demands a real Uint8Array: crypto.subtle, WebSocket.send(), fetch bodies, WebAssembly memory. Inside Node, Buffer.from(str, 'utf8') does the same job and is typically faster — but a Buffer is a Uint8Array subclass with extra methods, and some strict consumers reject it.
What trips people up
- UTF-8 only. There is no
new TextEncoder('utf-16le'); the constructor takes no arguments andencodingis always'utf-8'. OnlyTextDecoderaccepts an encoding label. - Byte length is not string length.
'héllo'.lengthis 5 but encodes to 6 bytes; emoji and CJK cost three or four each. Size buffers fromencode(str).length, never fromstr.length. encodeInto()truncates silently. Given a shortdestit fills what fits, reports{ read, written }, and never splits a character across the boundary — checkreadagainstsrc.lengthrather than assuming it completed.- Lone surrogates are replaced with U+FFFD rather than throwing.
Example
When to use it
- Hash or sign a string with crypto.subtle, which accepts bytes rather than text.
- Send a text payload over a WebSocket or as a fetch body in code shared with the browser.
- Copy a string into WebAssembly linear memory using encodeInto and a pre-allocated view.
Frequently asked questions
What is the difference between TextEncoder and Buffer.from in Node?
They produce the same UTF-8 bytes; only the wrapper differs. `TextEncoder` returns a plain `Uint8Array` and is part of the web standard, so it works in browsers and edge runtimes, while `Buffer.from(str, 'utf8')` returns a Node-specific `Uint8Array` subclass with extra methods like `toString('base64')` and is usually faster.
How do I get the UTF-8 byte length of a string?
Use `new TextEncoder().encode(str).length`, or `Buffer.byteLength(str, 'utf8')` in Node, which measures without allocating. Do not use `str.length` — it counts UTF-16 code units, so `'héllo'` reports 5 while the encoding is 6 bytes and an emoji reports 2 while costing 4.
Can TextEncoder encode UTF-16 or Latin-1?
No. The constructor takes no arguments and `encoding` is permanently `'utf-8'`; that is fixed by the WHATWG Encoding Standard. Decoding is the asymmetric side — `new TextDecoder('utf-16le')` and other labels are supported for reading bytes back into a string.
More examples
TextEncoder versus Buffer.from in Node
Both produce identical bytes; the difference is the wrapper. Choose `TextEncoder` for portable code and for APIs that reject `Buffer`; choose `Buffer.from` inside Node when you want `toString('hex')`, `toString('base64')` or `Buffer.byteLength` (which measures without allocating). The final line is the zero-copy bridge — `Buffer.from(arrayBuffer, byteOffset, length)` creates a view over the encoder's existing memory rather than copying it.
const encoder = new TextEncoder();
const viaEncoder = encoder.encode('héllo');
const viaBuffer = Buffer.from('héllo', 'utf8');
console.log(viaEncoder); // Uint8Array(6) [ 104, 195, 169, 108, 108, 111 ]
console.log([...viaBuffer]); // [ 104, 195, 169, 108, 108, 111 ] (same bytes)
// A Buffer IS a Uint8Array, but not vice versa.
console.log(viaBuffer instanceof Uint8Array); // true
console.log(Buffer.isBuffer(viaEncoder)); // false
// Byte length without allocating: Buffer wins here.
console.log(Buffer.byteLength('héllo', 'utf8')); // 6
console.log(encoder.encode('héllo').length); // 6, but allocates
// Wrap the encoder output when a Buffer API is required, with no copy:
const asBuffer = Buffer.from(viaEncoder.buffer, viaEncoder.byteOffset, viaEncoder.byteLength);
console.log(asBuffer.toString('hex')); // 68c3a96c6c6fEncoding for Web Crypto and other byte-oriented APIs
`crypto.subtle` accepts only a `BufferSource`, which is why `TextEncoder` appears in nearly every Web Crypto snippet. Encoding both the key material and the message with the same encoder guarantees the byte sequence matches what a browser or an edge runtime would produce for the identical input — the whole point of using the standard class rather than `Buffer` in code that has to agree across runtimes. Reuse one encoder instance; construction is cheap but not free.
const encoder = new TextEncoder();
async function sha256Hex(message) {
// crypto.subtle requires a BufferSource, not a string.
const data = encoder.encode(message);
const digest = await crypto.subtle.digest('SHA-256', data);
return [...new Uint8Array(digest)]
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
sha256Hex('hello').then(console.log);
// 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
// The same encoder feeds HMAC keys, WebSocket frames and fetch bodies.
async function sign(key, message) {
const cryptoKey = await crypto.subtle.importKey(
'raw', encoder.encode(key),
{ name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],
);
const sig = await crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(message));
return Buffer.from(sig).toString('base64');
}encodeInto for fixed buffers, and how it truncates
`encodeInto` exists to avoid an allocation per encode — you keep one scratch buffer and refill it — which matters in a hot loop or a streaming parser. Its failure mode is the catch: an undersized destination produces no error at all, just a smaller `read`. It will never emit a partial multi-byte character, so the buffer always holds valid UTF-8, but the string is quietly cut short unless you compare `read` against `src.length` as the helper does.
const encoder = new TextEncoder();
// Room for everything.
const big = new Uint8Array(16);
console.log(encoder.encodeInto('héllo', big));
// { read: 5, written: 6 } all 5 code units read, 6 bytes written
// Not enough room: it fills what fits and reports how far it got.
const small = new Uint8Array(2);
console.log(encoder.encodeInto('héllo', small), small);
// { read: 1, written: 1 } Uint8Array(2) [ 104, 0 ]
// Only 'h' fits: 'é' needs 2 bytes and would not fit in the remaining 1,
// so it is NOT split across the boundary.
// Always verify completeness rather than assuming it.
function encodeIntoChecked(src, dest) {
const { read, written } = encoder.encodeInto(src, dest);
if (read < src.length) {
throw new RangeError(`buffer too small: encoded ${read}/${src.length} code units`);
}
return written;
}
try {
encodeIntoChecked('héllo', new Uint8Array(2));
} catch (err) {
console.log(err.message); // buffer too small: encoded 1/5 code units
}
Discussion