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.
Recipe
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:
- Parsing upload filenames with non-ASCII characters
- Converting
fetchresponse bytes to strings - Validating CSV/XML declared as UTF-8 from third parties
- Interoping with browser
TextEncoderin isomorphic code
Working Example
import { 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: truerejects bad sequences - right for security-sensitive parsersfatal: falsesubstitutes U+FFFD - OK for user-generated display with warningsTextEncoderonly supports UTF-8 per spec - useiconv-litefor legacy encodingsBuffer.toString('utf8')replaces invalid bytes by default - similar to non-fatal decoder
Deep Dive
How It Works
- JavaScript strings are UTF-16 internally; UTF-8 is the interchange encoding for I/O.
- BOM - UTF-8 BOM (
EF BB BF) is optional; strip when parsing JSON that must start with{. - Normalization - NFC vs NFD matters for string comparison (usernames, filenames).
- Buffer encodings -
hex,base64,base64url,latin1for specific protocols - not general text.
Encoding Selection
| 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 |
TypeScript Notes
function assertUtf8Json(raw: Buffer): unknown {
const text = new TextDecoder('utf-8', { fatal: true }).decode(raw);
return JSON.parse(text);
}Gotchas
- Assuming
binarystring mode preserves bytes - legacy footgun. Fix: useBufferorUint8Arrayfor binary. - Double encoding UTF-8 - encoding already-valid strings for "safety". Fix: encode once at system boundary.
- Ignoring charset in Content-Type - ISO-8859-1 defaults in old clients. Fix: force UTF-8 end-to-end.
- Filename encoding on multipart - RFC 5987
filename*UTF-8 percent encoding. Fix: use framework parsers tested for i18n. - Comparing unnormalized Unicode - visually identical usernames differ at byte level. Fix:
str.normalize('NFC')before compare.
Alternatives
| 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 |
FAQs
Is UTF-8 default in Node?
Buffer.toString() without encoding uses utf8. HTTP should declare charset=utf-8 explicitly.
What does fatal true do?
Throws TypeError on invalid byte sequences instead of inserting replacement characters.
How do I detect encoding automatically?
chardet libraries guess - prefer explicit UTF-8 contract; guessing is risky for security.
Does JSON require UTF-8?
JSON text is Unicode - UTF-8 is standard on the wire. JSON.parse expects proper Unicode escapes in strings.
What is base64url?
URL-safe base64 variant - Buffer.from(s, 'base64url') for JWT segments.
How long is a string in bytes?
Buffer.byteLength(str, 'utf8') - not str.length (UTF-16 code units).
Can TextDecoder stream?
decode(uint8, { stream: true }) for incremental parsing of chunked input.
What about emoji and UTF-8?
Emoji are multi-byte in UTF-8 - still valid; grapheme clusters need special handling for "length" limits.
How do I strip UTF-8 BOM?
If bytes[0]===0xEF && bytes[1]===0xBB && bytes[2]===0xBF, slice from offset 3 before JSON.parse.
Is latin1 ever correct?
Only for true single-byte legacy protocols - never for user text in new APIs.
How does fetch handle encoding?
response.text() uses UTF-8 per WHATWG; use arrayBuffer() for binary.
What errors do clients see on fatal decode?
Map to 400 invalid_encoding - do not echo raw bytes in error messages.
Related
- Buffers Basics - allocation and slices
- Binary Protocols - non-text payloads
- Buffer Security - leaks in error messages
- Streaming HTTP Responses - charset headers
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.