TextDecoder and TextEncoder

Sometimes the binary data you’re holding isn’t really “binary” at all — it’s text that happens to travel as bytes. You fetched a file, read a network response, or pulled a chunk off a stream, and what you actually want is a readable string.

That’s the gap TextDecoder and TextEncoder fill. One turns bytes into a string, the other turns a string into bytes. Both are built into the browser (and Node.js), so there’s nothing to install.

bytes
[67, 111, 100, 101, 120]
TextDecoder →
← TextEncoder
string
“Codex”
The two directions: decode reads bytes into text, encode writes text back to bytes.

Decoding bytes into a string

The built-in TextDecoder object reads a buffer and hands you back a real JavaScript string, based on the encoding you tell it to use.

You create one first:

let decoder = new TextDecoder([label], [options]);
  • label — the encoding. Defaults to utf-8, but you can also ask for big5, windows-1251, and many others.
  • options — an optional settings object:
    • fatal — boolean. When true, the decoder throws on bytes it can’t decode. When false (the default), it quietly swaps each bad sequence for the replacement character (that’s the `` character you sometimes see).
    • ignoreBOM — boolean. When true, the decoder keeps the optional byte-order mark in the output instead of stripping it. Rarely needed.

Then you decode:

let str = decoder.decode([input], [options]);
  • input — the BufferSource to decode. That means an ArrayBuffer or any typed-array view over one (like Uint8Array).
  • options — an optional settings object:
    • stream — set to true when you’re feeding data in chunks and calling decode repeatedly. A single character can span more than one byte, and a chunk boundary might fall right in the middle of one. This flag tells the decoder to hold on to the “unfinished” bytes and finish decoding them when the next chunk arrives.

Here’s the simplest possible case — five ASCII bytes:

let uint8Array = new Uint8Array([67, 111, 100, 101, 120]);

alert( new TextDecoder().decode(uint8Array) ); // Codex

Each byte maps cleanly to one character because those code points all live in the ASCII range, where UTF-8 uses exactly one byte per character.

67
C
111
o
100
d
101
e
120
x
ASCII characters are one byte each in UTF-8, so the mapping is one-to-one.

Non-ASCII text works too, but the byte count no longer matches the character count. Japanese kanji, for example, sit high enough in Unicode that UTF-8 needs three bytes each:

let uint8Array = new Uint8Array([230, 151, 165, 230, 156, 172]);

alert( new TextDecoder().decode(uint8Array) ); // 日本

Six bytes, two characters. The decoder reads the leading byte of each sequence, sees how many continuation bytes should follow, and groups them accordingly.

230151165
230156172
In UTF-8, characters above the ASCII range use multiple bytes. Here each character takes three.

Decoding just part of a buffer

You don’t have to decode the whole thing. If the text you care about sits in the middle of a larger buffer — say, wrapped in null bytes — you can make a view over just that slice and decode the view. No copying involved:

let uint8Array = new Uint8Array([0, 67, 111, 100, 101, 120, 0]);

// the string sits in the middle, flanked by zero bytes
// create a view over just that region — nothing is copied
let binaryString = uint8Array.subarray(1, -1);

alert( new TextDecoder().decode(binaryString) ); // Codex

subarray(1, -1) starts at index 1 and stops one before the end, so the leading and trailing 0 are excluded. The returned view shares the same underlying memory as the original array — it’s a window, not a duplicate.

0
67
111
100
101
120
0
the boxed region → “Codex”
subarray(1, -1) is a view over the middle of the same memory; the padding bytes stay outside the window.

TextEncoder

TextEncoder runs the process in reverse — it converts a string into bytes.

The syntax has no options to fuss over:

let encoder = new TextEncoder();

The only encoding it supports is utf-8. If you need to produce bytes in some other encoding, TextEncoder won’t do it — that’s a deliberate limitation of the API.

It gives you two methods:

  • encode(str) — returns a fresh Uint8Array holding the UTF-8 bytes of str.
  • encodeInto(str, destination) — writes the bytes straight into an existing Uint8Array you pass as destination, instead of allocating a new one. Handy when you’re managing your own buffer and want to avoid extra allocations.
let encoder = new TextEncoder();

let uint8Array = encoder.encode("Codex");
alert(uint8Array); // 67,111,100,101,120

Encode "Codex" and you get back exactly the byte sequence you started this article decoding — the two operations are inverses.