Unicode and Why Text Breaks
Text looks effortless until a customer name displays as âĂ©â, a password with an emoji fails validation, or a tweet counter disagrees with your app. Those bugs rarely come from âweird charactersââthey come from mixing up three different ideas: how Unicode names characters, how encodings store them as bytes, and what humans perceive as one character on screen. One visible glyph is not one byte and not always one JavaScript string index.
6 min read · Updated 2026-10-04

Unicode assigns numbers; encodings store bytes
The Unicode Standard assigns a code pointâa number, written like U+0041 for Latin capital Aâto each abstract character in its catalog. The catalog is not the same as a font: fonts map code points to glyphs. UTF-8, UTF-16, and UTF-32 are encoding schemes that turn code point sequences into bytes for files, HTTP, and databases. Bugs appear when software treats UTF-8 bytes as if they were Latin-1 characters, or when two systems assume different encodings without declaring them.
Same character, different representations
| View | Example for Ă© (U+00E9) |
|---|---|
| Code point | U+00E9 (single assigned character) |
| UTF-8 bytes (hex) | C3 A9 (two bytes) |
| UTF-16 code units (hex) | 00 E9 (one unit in BMP) |
| JavaScript string length | 1 (one UTF-16 code unit for BMP Ă©) |
UTF-8 in practice
UTF-8 is variable-width: ASCII letters use one byte; many European scripts use two; most emoji and rare scripts use four. It is the dominant encoding on the web (HTTP, JSON, HTML). When a system says âUnicodeâ in 2026, it usually means âUTF-8 on the wire unless stated otherwise.â
Character: Ă©
UTF-8 bytes: 0xC3 0xA9
Mis-decoded as Latin-1: à © â often shown as "Ă©" (mojibake)UTF-16 and JavaScript strings
JavaScript strings are sequences of UTF-16 code units, not Unicode code points. Characters outside the Basic Multilingual Plane (code points above U+FFFF)âmany emoji, rare Han charactersâtake two UTF-16 code units called a surrogate pair. That means String.length counts code units, not user-visible characters, and indexing with str[i] can split a surrogate pair and produce garbage.
const smile = "đ";
console.log(smile.length); // 2 (surrogate pair)
console.log(smile[0] === smile[1]); // false
console.log([...smile].length); // 1 (iterates code points)
console.log(Array.from(smile).length); // 1Spread and Array.from use Unicode code point iteration. For production limits, prefer Intl.Segmenter or a grapheme-aware library when counting what users see.
Grapheme clusters: what users count
A user-visible character may combine multiple code points. A base letter plus combining acute accent (e + âÌ) can render like Ă© but remain two code points. Emoji flags are pairs of regional-indicator letters; skin-tone modifiers attach to person emoji; ZWJ sequences join family emoji. Unicode defines extended grapheme cluster boundaries for âwhat feels like one characterâ in editing and cursor movement.
One glyph, multiple code points
| Visual | Code points (conceptual) | JS .length pitfall |
|---|---|---|
| Ă© (precomposed) | U+00E9 | 1 code unit |
| Ă© (e + combining acute) | U+0065 U+0301 | 2 code units |
| đšâđ©âđ§âđŠ (family ZWJ) | Several code points + ZWJ U+200D | .length â« 1 |
| đ«đ· (flag) | Two regional indicators | .length === 4 |
| 1ïžâŁ (keycap digit) | U+0031 U+FE0F U+20E3 | .length === 3 |
| đđœ (skin tone) | U+1F44D U+1F3FD | .length === 4 |
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const text = "e\u0301"; // e + combining acute
console.log(text.length); // 2
console.log([...segmenter.segment(text)].length); // 1 graphemeconst bio = "Hi đšâđ©âđ§âđŠ!"; // family ZWJ sequence
console.log(bio.length); // 11 UTF-16 code units
console.log([...bio].length); // 8 Unicode code points
console.log([...new Intl.Segmenter("en", { granularity: "grapheme" }).segment(bio)].length); // 7 graphemes (letters, space, family emoji, !)
// A "max 10 characters" rule using .length rejects this bio even though it looks short.Mojibake and fixing encoding mistakes
Mojibake (garbled text) usually means bytes were interpreted with the wrong decoder. Classic case: UTF-8 bytes read as Windows-1252 or ISO-8859-1. The fix is to re-read the original byte stream with the encoding that produced it, then transcode to UTF-8ânot to manually replace âĂ©â with âĂ©â in a corrupted string that lost information.
Bytes in
File upload, HTTP body, database BLOB
Detect or declare encoding
BOM, Content-Type charset, metadata
Decode to Unicode
In-memory code points in your language
Process
Search, validate, displayâon Unicode text
Encode on output
Almost always UTF-8 for web APIs
Convert at the edges; keep Unicode in the middle.
Emoji and length limits
Product limitsââ140 charactersâ, SMS segments, database VARCHARâpick a counting rule. Twitter-style counts historically used weighted code units; SMS uses GSM-7 or UCS-2 segments; databases may count bytes. An emoji-heavy message can exceed a byte budget while looking short visually. Document which metric your counter exposes: bytes, UTF-16 units, code points, or graphemes.
- Storage limits (PostgreSQL bytea vs char length) often care about bytes in UTF-8.
- JavaScript maxlength on inputs often counts UTF-16 code units.
- Regulations or billing may count graphemes or legacy code pages.
- Base64 expands binary UTF-8âencoding text for URLs is not the same as character count.
Normalization: when equality fails
The same visual string can have multiple Unicode representations. NFC and NFD normalization forms pick canonical precomposed vs decomposed sequences. Passwords and usernames should be compared after agreed normalizationâor you lock out users whose IME produces a different but equivalent sequence.
Case folding and locale
Turkish dotted and dotless I, German Ă, and Greek sigma variants show why toLowerCase() alone is a weak equality strategy. Unicode case folding (case-fold) and locale-aware collation belong in search indexes and username policies, not ad hoc string hacks in application code.
Surrogates and invalid sequences
UTF-8 allows only well-formed byte sequences; lone surrogates are illegal in UTF-8 but can appear when UTF-16 data is mishandled. Parsers that silently substitute replacement characters (U+FFFD) may hide data corruptionâlog and reject invalid input at trust boundaries when integrity matters.
Checklist for developers
- Declare charset=utf-8 on HTML and JSON responses.
- Never assume one byte per character in buffer sizes.
- Do not use string length alone for user-facing limits.
- Use Intl.Segmenter or ICU for grapheme-aware editing limits.
- Test with combining marks, emoji flags, and ZWJ sequences in QA fixtures.
- When integrating legacy CSV or Windows exports, ask which encoding created the file.
SmartKitio character and word counters should be read alongside their documented counting modeâcompare results to your platformâs rules before shipping limits to production.
Binary data is not text
Base64 and hex encodings represent bytes, not Unicode strings. Decoding Base64 yields bytes that you must still decode with UTF-8 (or the correct charset) before treating the result as text. Piping binary through string APIs without an explicit encoding step recreates mojibake at a different layerâcommon in upload pipelines and email attachments.
What to remember
- Unicode names characters as code points; UTF-8 (and other encodings) store those points as bytes.
- JavaScript .length counts UTF-16 code unitsâsurrogate pairs and combining sequences break naive limits.
- Users count grapheme clusters; use Intl.Segmenter or ICU when product rules say âcharacters.â
- Mojibake is usually wrong decoding at a boundaryâfix the pipeline, not the string with search-and-replace.
- Normalize (usually NFC) before comparing passwords and usernames that may come from different IMEs.
Further reading
Related articles

Technology
Why Software Uses Different Data Formats
JSON, CSV, binary blobs, and images solve different constraintsâinterchange is never free.
6 min read · 2026-10-04
Read the article
Programming
Encoding vs Encryption: What's the Difference?
Encoding, encryption, hashing, and compression solve different problems. Confusing themâespecially Base64 with secrecyâcreates real security failures.
6 min read · 2026-10-04
Read the article
Web
Client-Side vs Server-Side Processing
Where computation runs shapes privacy, latency, cost, and trust. A practical map of browser work versus server workâand what hybrid apps actually do.
5 min read · 2026-10-04
Read the article
Web
Percentages in Everyday Life
Tips, tax, discounts, interest, stats, and headlinesâformulas, percentage points vs relative change, and worked examples you can verify.
6 min read · 2026-10-04
Read the article
