Network chunks do not necessarily align with character boundaries. If a UTF-8 character is split across chunks, decoding each piece independently can corrupt it. The Encoding Standard and MDN’s TextDecoder.decode() reference define the key behavior.

Conceptual diagram of a UTF-8 character split across chunks while a decoder carries state forward

Conceptual illustration, not a captured request or byte trace.

Keep one decoder across chunks

Use one TextDecoder instance and call decode(bytes, {stream:true}) for each nonfinal chunk. The decoder retains an incomplete trailing byte sequence for the next input. Without stream:true, each call treats its input as complete; creating a new decoder per chunk loses that state. The ReadableStream.pipeTo() reference provides context for stream closure and error propagation.

Flush when input ends

On normal end-of-input, call decoder.decode() once more to flush pending decoder state. Omitting the final call can leave an incomplete sequence unhandled. By default, fatal is false and malformed input can be replaced. With fatal:true, decoding errors throw so the caller can reject malformed data rather than silently accepting replacement output.

Do not promise one replacement per bad byte

In replacement mode, the exact output depends on the structure of the malformed sequence. Do not claim that every invalid byte produces exactly one replacement character. If decoded text feeds another parser, a signed payload, or a security-sensitive operation, choose an explicit input policy and decide whether fatal mode or separate validation is needed.

When to use TextDecoderStream

TextDecoderStream transforms a ReadableStream of byte chunks into text chunks and handles boundaries as the stream flows. Normal close flushes pending state; fatal errors can error the stream. Check browser support and how cancellation and errors propagate through the pipeline. It is useful when downstream code consumes chunks incrementally; it has little value if the whole response is already buffered before decoding.

Keep one decoder through the read loop

A reader loop can pass each Uint8Array from a response body to the same decoder, then call decode() once more when the reader reports done. If the decoded text is parsed into lines, do not assume byte chunks align with either UTF-8 characters or line endings; carry the trailing text fragment into the next decoded result. This describes the API flow and is not a claim that a live request was executed.

If reading is cancelled or fails, do not treat it as normal EOF or present partial data as a complete document. Log the termination reason, whether the final decode() flush ran, and whether fatal:true raised an exception; these help separate transport interruption from decoding failure. Define whether partial data is retried, discarded, or reported. With transform streams, also check how pipeTo() propagates errors and cancellation.

Sources