TOOLTIKI Lovable tool, really free

Why your emoji is two characters long

Type a single emoji into a JavaScript console and ask for its length. The answer is 2. Type a family emoji and the answer is 11. Neither is a bug, and understanding why explains a whole category of problems that otherwise look like magic.

The difficulty is that "character" means four different things, and different parts of your stack count different ones.

The four counts

Unit What it is 😀 👨‍👩‍👧
Grapheme what a person calls a character 1 1
Code point what Unicode assigns a number to 1 5
UTF-16 code unit what JavaScript .length counts 2 11
UTF-8 byte what a database column measures 4 25

For plain English all four are identical, which is precisely why the problem stays invisible until the first emoji or accented name arrives in production.

Why is JavaScript counting two?

Because JavaScript strings are UTF-16, a format designed when Unicode was believed to need 65,536 code points. It turned out to need more, and the fix was surrogate pairs: code points above U+FFFF are stored as two 16-bit units.

So an emoji occupies two slots in a JavaScript string. .length reports slots, not characters.

The practical consequence is slicing. Cutting a string at a fixed length can land between the two halves of a surrogate pair, leaving a lone surrogate that is not a character at all — which is where the replacement diamond in a truncated tweet comes from.

Iterating with for...of or spreading into an array walks code points rather than units, which fixes most of it.

Why is a family five code points?

Because it is not one character. It is a man, a woman and a girl joined by zero-width joiners — invisible code points whose only job is to say "render these as one glyph".

That is why deleting one sometimes takes several presses of backspace, and why a character counter can report a number wildly different from what you see. It is also why a naive truncation can leave you with half a family.

Why does my database reject emoji?

This one has a specific and very common cause. MySQL has an encoding confusingly named utf8 which stores at most three bytes per character. Every emoji needs four.

The column has to be utf8mb4 — the "mb4" is literally "maximum bytes 4". An emoji silently disappearing between a form submission and the page is nearly always this, and the data is genuinely gone rather than hidden.

The name is a historical mistake that MySQL has spent years trying to correct; newer versions treat utf8 as an alias for utf8mb4, but plenty of existing schemas predate that.

Which count should I use for a limit?

It depends what the limit is protecting.

A database column limit is bytes. A display limit — "no more than 40 characters" — should be graphemes, because that is what a reader perceives. An API limit is whatever the API documented, which is worth checking rather than assuming.

Twitter is the famous example of getting this right: its limit counts code points with a weighting, not bytes and not UTF-16 units, so a Japanese post is not penalised for using a script where each character carries more meaning.

Questions people ask

Why do two identical-looking strings not match? Probably normalisation — an accented character has more than one valid encoding. That is a related problem with its own fix.

Is \uD83D a valid character? No. It is half a surrogate pair. Alone it is invalid and most software will show a replacement glyph.

How do I count graphemes properly? Intl.Segmenter with grapheme granularity, which is now available everywhere that matters.

Why does my regex not match an emoji? Because . matches a code unit unless you use the u flag. With it, . matches a code point.

The Unicode converter shows all four counts and the breakdown per character, the Unicode escape converter handles the escapes, and the Unicode normaliser deals with the identical-strings-that-differ problem.