util.TextEncoder

Encodes a JavaScript string into a Uint8Array of UTF-8 bytes.

Signature
new util.TextEncoder()
  • Added in Node.js v8.3.0

Parameters

ParameterTypeDescription
$encode.inputoptionalstring= '' (an empty string)Passed to textEncoder.encode([input]): the text to encode. Returns a Uint8Array of UTF-8 bytes.
$encodeInto.srcstringPassed to textEncoder.encodeInto(src, dest): the text to encode.
$encodeInto.destUint8ArrayPassed 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 and encoding is always 'utf-8'. Only TextDecoder accepts an encoding label.
  • Byte length is not string length. 'héllo'.length is 5 but encodes to 6 bytes; emoji and CJK cost three or four each. Size buffers from encode(str).length, never from str.length.
  • encodeInto() truncates silently. Given a short dest it fills what fits, reports { read, written }, and never splits a character across the boundary — check read against src.length rather than assuming it completed.
  • Lone surrogates are replaced with U+FFFD rather than throwing.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · javascript
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')); // 68c3a96c6c6f

Encoding 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.

Example · javascript
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.

Example · javascript
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

  • Be the first to comment on this lesson.