Text Encodings
Text on the wire is bytes - UTF-8 is the default for Node 24 APIs, and TextEncoder/TextDecoder provide Web-standard conversion with explicit error handling for malformed input.
Search across all documentation pages
Text on the wire is bytes - UTF-8 is the default for Node 24 APIs, and TextEncoder/TextDecoder provide Web-standard conversion with explicit error handling for malformed input.
const encoder = new TextEncoder(); // always UTF-8
const decoder = new TextDecoder('utf-8', { fatal: true });
const bytes = encoder.encode('Hello 世界');
const text = decoder.decode(bytes);const buf = Buffer.from('café', 'utf8');
console.log(buf.toString('utf8'));When to reach for this:
fetch response bytes to stringsTextEncoder in isomorphic codeimport { Buffer } from 'node:buffer';
function decodeUtf8Strict(bytes: Uint8Array): string {
const decoder = new TextDecoder('utf-8', { fatal: true });
try {
return decoder.decode(bytes);
} catch {
throw new Error('Invalid UTF-8 sequence');
}
}
function decodeUtf8Lossy(bytes: Uint8Array): string {
return new TextDecoder('utf-8', { fatal: false }).decode(bytes);
}
const valid = Buffer.from('hello', 'utf8');
const invalid = Buffer.from([0xff, 0xfe, 0xfd]);
console.log(decodeUtf8Strict(valid));
console.log(decodeUtf8Lossy(invalid)); // replacement chars// HTTP Content-Type should specify charset=utf-8
const encoder = new TextEncoder();
const body = encoder.encode(JSON.stringify({ greeting: '你好' }));What this demonstrates:
fatal: true rejects bad sequences - right for security-sensitive parsersfatal: false substitutes U+FFFD - OK for user-generated display with warningsTextEncoder only supports UTF-8 per spec - use iconv-lite for legacy encodingsBuffer.toString('utf8') replaces invalid bytes by default - similar to non-fatal decoderEF BB BF) is optional; strip when parsing JSON that must start with {.hex, base64, base64url, latin1 for specific protocols - not general text.| Encoding | Use |
|---|---|
| UTF-8 | Default for HTTP, JSON, files |
| hex / base64 | Binary in text channels |
| latin1 | Legacy 1-byte - avoid for new text |
| iconv | Shift_JIS, Windows-1252 legacy |
function assertUtf8Json(raw: Buffer): unknown {
const text = new TextDecoder('utf-8', { fatal: true }).decode(raw);
return JSON.parse(text);
}binary string mode preserves bytes - legacy footgun. Fix: use Buffer or Uint8Array for binary.filename* UTF-8 percent encoding. Fix: use framework parsers tested for i18n.str.normalize('NFC') before compare.| Alternative | Use When | Don't Use When |
|---|---|---|
iconv-lite | Legacy Windows/Asian encodings | New UTF-8-only APIs |
Buffer.toString('utf8') | Quick Node-only scripts | Need Web API parity |
| Streams + transform | Huge text files | Small string conversions |
util.TextDecoder legacy import | - | Use global TextDecoder in Node 24 |
Buffer.toString() without encoding uses utf8. HTTP should declare charset=utf-8 explicitly.
Throws TypeError on invalid byte sequences instead of inserting replacement characters.
chardet libraries guess - prefer explicit UTF-8 contract; guessing is risky for security.
JSON text is Unicode - UTF-8 is standard on the wire. JSON.parse expects proper Unicode escapes in strings.
URL-safe base64 variant - Buffer.from(s, 'base64url') for JWT segments.
Buffer.byteLength(str, 'utf8') - not str.length (UTF-16 code units).
decode(uint8, { stream: true }) for incremental parsing of chunked input.
Emoji are multi-byte in UTF-8 - still valid; grapheme clusters need special handling for "length" limits.
If bytes[0]===0xEF && bytes[1]===0xBB && bytes[2]===0xBF, slice from offset 3 before JSON.parse.
Only for true single-byte legacy protocols - never for user text in new APIs.
response.text() uses UTF-8 per WHATWG; use arrayBuffer() for binary.
Map to 400 invalid_encoding - do not echo raw bytes in error messages.
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.
Reviewed by Chris St. John·Last updated Jul 16, 2026