TextDecoder
new TextDecoder(encoding) TextDecoder turns raw bytes into a JavaScript string. It is part of the Encoding API and the counterpart to TextEncoder. In browsers it lives on window. It also works in Web Workers. Every modern JavaScript environment supports it.
Constructor
new TextDecoder(encoding = "utf-8", options)
The constructor accepts two arguments: an encoding label and an optional options object. Most of the time you can omit both and get a standard UTF-8 decoder, but the options give you control over error handling and byte-order-mark behavior.
Parameters
encoding(string) — A valid WHATWG encoding label. Defaults to"utf-8". Case-insensitive, so"UTF-8"and"utf-8"work the same.options(object) — Optional configuration:fatal(boolean) — Whentrue,decode()throws aTypeErroron malformed bytes. Defaults tofalse(replaces bad bytes with the Unicode replacement characterU+FFFD).ignoreBOM(boolean) — Whentrue, skips the byte order mark. Defaults tofalse.
Throws
RangeErrorif the encoding label is not a recognized WHATWG label.
// Default UTF-8 decoder
const decoder = new TextDecoder();
// Decoder for a specific encoding
const latinDecoder = new TextDecoder("iso-8859-1");
// Strict decoder — throws on invalid bytes
const strictDecoder = new TextDecoder("utf-8", { fatal: true });
After construction, the decoder exposes its resolved encoding name through a read-only property. This is useful for confirming which encoding is actually in use, since label aliases get normalized during initialization. You can check it before calling decode() to verify your label was accepted.
encoding
dec.encoding // "utf-8"
The .encoding property is read-only and reflects the canonical name after label resolution. This means you can safely read it to confirm which encoding a decoder is actually using, regardless of what label string you passed to the constructor. For instance, passing the legacy label "ISO-8859-1" produces a decoder whose .encoding reports "windows-1252" — the WHATWG canonical name. The following snippet demonstrates this normalization in practice.
const dec = new TextDecoder("ISO-8859-1");
console.log(dec.encoding); // "windows-1252"
Once you know which encoding the decoder resolved to, you can start feeding it byte data. The decode() method is the workhorse of the API — it accepts any buffer-like object and returns the corresponding string, with options for streaming and error handling.
decode()
decode(buffer, options)
Parameters
buffer(ArrayBuffer | TypedArray | DataView) — The encoded byte data to decode.options(object) — Optional:stream(boolean) — Settruewhen decoding a chunk of a larger stream and more data will follow. Omit or setfalsefor the final chunk (or a complete buffer in one call).
Returns string — The decoded text.
Throws
TypeErroriffatal: truewas set and the buffer contains invalid data for the given encoding.
The simplest case is decoding a complete buffer in one shot. The result is a standard JavaScript string ready for display or further processing.
const bytes = new Uint8Array([72, 101, 108, 108, 111]);
const text = new TextDecoder().decode(bytes);
console.log(text); // "Hello"
The output is straightforward for valid UTF-8 input — no surprises. But what happens when the bytes are corrupt? The decoder’s behavior depends entirely on the fatal option you set during construction. Understanding the difference helps you decide when to use each mode.
Fatal mode
With fatal: true, malformed bytes cause an immediate error instead of silent substitution:
const bad = new Uint8Array([0x80]); // not valid UTF-8
// Default: replacement character inserted
const result = new TextDecoder().decode(bad);
console.log(result.length); // 1 — one U+FFFD character
// Strict: throws
try {
new TextDecoder("utf-8", { fatal: true }).decode(bad);
} catch (e) {
console.log(e instanceof TypeError); // true
}
Fatal mode catches corruption early — ideal for protocols and file formats that must be byte-perfect. When you are dealing with data that arrives in pieces, however, you need the decoder to maintain state across calls instead of resetting after each chunk. The streaming API handles this case.
Streaming decode
When processing data in chunks (for example, from a file or network stream), pass stream: true on all chunks except the last. This tells the decoder to hold state between calls instead of flushing after each chunk:
const chunks = [
new Uint8Array([0x48, 0x69]), // "Hi"
new Uint8Array([0x20, 0x74, 0x68]), // " th"
new Uint8Array([0x65, 0x72, 0x65]), // "ere"
];
const decoder = new TextDecoder();
let result = "";
for (const chunk of chunks) {
result += decoder.decode(chunk, { stream: true });
}
result += decoder.decode(); // final chunk — no buffer, stream: false by default
console.log(result); // "Hi there"
A common mistake is using stream: true on the final chunk. When you do that, the decoder waits for more data and returns an empty string. Always call decode() with no arguments (or stream: false) to flush the final chunk.
Common Gotchas
Default fatal: false silently replaces bad bytes. If you need strict validation, you must pass { fatal: true } to the constructor. Most developers never notice corrupted data because the replacement character looks like a normal character in casual inspection.
Encoding labels are normalized. The WHATWG spec maps many label aliases to canonical names. "utf-8" and "UTF-8" both produce a decoder with .encoding === "utf-8". Some legacy labels map to different encodings — "iso-8859-1" actually maps to "windows-1252".
BOM is always skipped. The UTF-8 byte order mark (0xEF 0xBB 0xBF) does not appear in the output regardless of how you set ignoreBOM. There is no option to include it.
stream: true on the final chunk returns an empty string. This trips up developers processing a fixed set of chunks — the last chunk should use decode() with no options or stream: false.
See Also
- TextEncoder — Encodes strings to UTF-8 bytes (the inverse operation)
- ArrayBuffer — The raw binary container you pass to
decode() - TypedArray — Typed array types used to represent the byte input