JavaScript String Byte Length: Why .length Is Wrong & How to Get It Right

str.length counts UTF-16 code units, not bytes and not even characters. If you're validating a string against a byte-based limit (a database column, an API payload cap), .length will pass strings that are actually too long the moment emoji, CJK, or accented text is involved.

The three numbers, and why they differ

A JavaScript string is stored as UTF-16. .length counts 16-bit code units. Most non-ASCII limits you actually hit — a database column, a network payload, an S3 key — are byte-based and almost always UTF-8. These numbers only agree for plain ASCII text.

String.lengthCode pointsUTF-8 bytes
"a"111
"café"445
"日本語" (3 CJK characters)339
"😀" (one emoji)214

The emoji row is the one that trips people up: "😀".length is 2, not 1 — most emoji sit outside the Basic Multilingual Plane, so JavaScript represents them as a surrogate pair of two UTF-16 code units. That's still not the byte count: the same character is 4 bytes in UTF-8. All three numbers above were computed with Node's own Buffer.byteLength and Array.from, not estimated.

Get the real UTF-8 byte length

In the browser (or any modern JS runtime): use TextEncoder, a built-in with no dependency:

new TextEncoder().encode(str).length // UTF-8 byte length

In Node.js: Buffer.byteLength is the standard way, and defaults to UTF-8:

Buffer.byteLength(str, "utf8")

Get the code point count (not the same as .length either)

If you want to count actual characters rather than UTF-16 units — still not bytes, but closer to "how many characters is this" — spread the string, which iterates by code point instead of by code unit:

Array.from(str).length // code points, handles surrogate pairs correctly
str.length              // WRONG for this: counts surrogate pairs as 2

Note that even code point count isn't the same as what a person would call "how many characters does this look like" — a flag emoji or a family emoji built from a Zero-Width-Joiner sequence is visually one character but multiple code points. If you need that, you want grapheme clusters via Intl.Segmenter, which is what the counter below reports as "visual" count.

Validating a byte-based limit (a VARCHAR column, an API's payload cap) with str.length means any input containing emoji, CJK, or many accented characters can pass client-side validation and then get truncated or rejected by the actual byte-based check further down the stack — a class of bug that only shows up with non-English or emoji-containing input, so it's easy to miss in English-only testing.

Check it live

Paste text into the counter below to see all four numbers (bytes, .length, code points, grapheme count) at once, plus whether it fits common byte limits like MySQL's index limits or DynamoDB's item size cap.

→ Open the UTF-8 byte counter