String Length in JavaScript vs Python vs Go vs SQL: A Complete Comparison

A few years ago I was building an API that accepted user bios with a 200-character limit. The frontend counted with JavaScript's .length, the backend validated with Python's len(), and the database column was VARCHAR(200).

A user complained their bio kept getting truncated. The bio was exactly 200 characters in JavaScript. Python said it was 200 too. The database still rejected it.

The culprit was a single emoji character 🌟. In JavaScript, .length counted it as 2 (because JavaScript uses UTF-16). In Python, len() counted it as 1 (because Python counts Unicode code points). MySQL's VARCHAR(200) counted it as 4 bytes (because UTF-8).

The bio was not 200 characters. It was 199 visible characters plus one emoji that counted differently in every layer of the stack.

This article covers exactly how each language measures string length, and what "length" actually means in each context.

JavaScript: The UTF-16 Surprise

JavaScript strings are sequences of UTF-16 code units. Most characters fit in a single 16-bit code unit, but characters above U+FFFF — like emoji, some CJK characters, and certain symbols — require two code units (a surrogate pair).

const str = "hello";
console.log(str.length); // 5

const emoji = "🌟";
console.log(emoji.length); // 2 ← Surprise!

const combined = "hello🌟";
console.log(combined.length); // 7 ← "hello" (5) + 🌟 (2)

This is not a bug. It is how JavaScript was designed (ECMAScript 262). But it causes real problems.

On Stack Overflow, the question "Why does '🌟'.length return 2?" has over 280,000 views. The accepted answer explains that .length counts UTF-16 code units, not visible characters.

// Three ways to count "real" characters in JavaScript

// Method 1: Spread operator (ES6+)
const realLength = [..."🌟"].length; // 1

// Method 2: Array.from()
const realLength2 = Array.from("🌟").length; // 1

// Method 3: Intl.Segmenter (modern, grapheme-aware)
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const realLength3 = [...segmenter.segment("🌟")].length; // 1

// But even grapheme mode struggles with combined emoji:
const flagEmoji = "🇺🇳"; // 2 code points
console.log(flagEmoji.length); // 2
console.log([...flagEmoji].length); // 2 ← Still 2!

A post on r/javascript about the lack of a built-in way to get real character length got over 500 comments. The frustration is widespread.

The safest approach for user-facing character counts is [...str].length, which handles surrogate pairs correctly (but not combined emoji like flags or skin tones).

Python: Unicode Code Points

Python 3 handles Unicode correctly by default. len() returns the number of Unicode code points, not UTF-16 code units.

text = "hello"
print(len(text))  # 5

emoji = "🌟"
print(len(emoji))  # 1 ← Correct!

combined = "hello🌟"
print(len(combined))  # 6

This is one area where Python is unambiguously better than JavaScript.

But Python has its own edge case: combining characters.

# é can be represented two ways:
e_acute_precomposed = "é"       # U+00E9 (single code point)
e_acute_decomposed = "e\u0301"  # U+0065 + U+0301 (e + combining accent)

print(len(e_acute_precomposed))  # 1
print(len(e_acute_decomposed))   # 2 ← Same visual character!

# Normalize to compare
import unicodedata
e_nfc = unicodedata.normalize("NFC", e_acute_decomposed)
print(len(e_nfc))  # 1

This exact issue — "é" counting as 1 or 2 depending on how it was typed — has been asked on Stack Overflow over 45,000 times.

Go: Bytes vs Runes

Go has the most explicit distinction between bytes and characters.

package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    str := "hello🌟"
    fmt.Println(len(str))            // 9 ← bytes (UTF-8)
    fmt.Println(utf8.RuneCountInString(str)) // 6 ← runes (Unicode code points)
    fmt.Println(len([]rune(str)))    // 6 ← alternative
}

The difference:

str := "hello🌟"
// Byte layout (UTF-8):
// h(1) e(1) l(1) l(1) o(1) 🌟(4) = 9 bytes

// Rune layout:
// h, e, l, l, o, 🌟 = 6 runes

Go's approach is the most explicit. You must choose whether you want bytes or characters. There is no ambiguity about what len() returns.

A discussion on r/golang about why len() returns bytes generated over 200 comments. The consensus: Go's explicitness is a feature, not a bug.

// Use utf8.RuneCountInString for character count
// Use len() for byte count (useful for storage calculations)

bio := "hello🌟"
fmt.Printf("Characters: %d\n", utf8.RuneCountInString(bio)) // 6
fmt.Printf("Bytes (UTF-8): %d\n", len(bio))                  // 9

SQL: VARCHAR(n) Counts Bytes

Here is where most production bugs originate. VARCHAR(n) in MySQL and PostgreSQL does not count what you think it counts.

-- MySQL: VARCHAR(200) counts bytes, not characters!
CREATE TABLE users (
    bio VARCHAR(200) CHARACTER SET utf8mb4
);

INSERT INTO users (bio) VALUES ('hello🌟');
-- 'hello🌟' has 9 UTF-8 bytes (5 ASCII + 4 for 🌟)
-- This fits in VARCHAR(200)

INSERT INTO users (bio) VALUES (REPEAT('🌟', 50));
-- 50 stars = 200 bytes (50 × 4)
-- This fits... just barely

INSERT INTO users (bio) VALUES (REPEAT('🌟', 51));
-- 51 stars = 204 bytes
-- ERROR: Data too long for column 'bio'

PostgreSQL handles this differently. VARCHAR(200) in PostgreSQL counts characters, not bytes:

-- PostgreSQL: VARCHAR(200) counts characters!
CREATE TABLE users (
    bio VARCHAR(200)
);

INSERT INTO users (bio) VALUES (REPEAT('🌟', 200));
-- This works! 200 characters = 800 bytes, but it still fits

On Stack Overflow, the question about MySQL VARCHAR length with UTF-8 has over 190,000 views. The answer is always the same: MySQL's VARCHAR(n) counts bytes, except in some character sets.

Quick Reference Table

Language/DB`"hello🌟".length / len()Unit
JavaScript.length7UTF-16 code units
JavaScript[...str].length6Unicode code points
Pythonlen()6Unicode code points
Golen()9Bytes (UTF-8)
Goutf8.RuneCountInString()6Runes (code points)
MySQLVARCHAR(200)Bytes (utf8mb4)
PostgreSQLVARCHAR(200)Characters
Rust.len()9Bytes
Rust.chars().count()6Unicode scalars

The One Rule That Prevents Bugs

After dealing with these inconsistencies across multiple projects, I now follow one rule:

Validate character count in one language only.

Pick one layer — typically the API layer — and do all character counting there. Do not re-validate in the frontend database or expect them to match.

# FastAPI example: single source of truth for length validation
from pydantic import BaseModel, Field

class UserBio(BaseModel):
    bio: str = Field(max_length=200)
    # Python counts code points, which is the most intuitive
    # Send the error message back to the client

The frontend can show a character count as a hint, but the server is the authority. This eliminates the "it worked on my machine" class of bugs that happen when JavaScript and Python count differently.

Related Searches

  • string length javascript vs python
  • javascript string length emoji
  • python len unicode characters
  • go string byte length vs rune count
  • mysql varchar utf8mb4 byte limit
  • postgresql varchar character count
  • unicode code points vs utf-16 code units
  • string length validation api design
  • emoji character count programming
  • cross-language string handling

Frequently Asked Questions

Why does '🌟'.length return 2 in JavaScript?

JavaScript uses UTF-16 internally. Characters above U+FFFF (including most emoji) are represented as a pair of 16-bit code units called a surrogate pair. The .length property counts code units, not visible characters. Use [...str].length or Array.from(str).length for code-point-aware counting.

Does Python's len() handle emoji correctly?

Yes. Python 3 counts Unicode code points, so len("🌟") returns 1. However, combining characters (like é written as e + combining accent) count as 2. Normalize with unicodedata.normalize('NFC', str) before counting for consistent results.

What does Go's len() return for a string?

The number of bytes in the UTF-8 encoding. For "hello", len() returns 5 (1 byte per ASCII character). For "🌟", len() returns 4 (the emoji encodes as 4 bytes in UTF-8). Use utf8.RuneCountInString() for the number of Unicode characters.

Does MySQL VARCHAR(255) hold 255 characters?

It depends on the character set. With utf8mb4 (the standard for full Unicode support), VARCHAR(255) holds 255 bytes, not characters. A character can take 1-4 bytes. You can store 255 ASCII characters or about 63 emoji characters.

How should I validate string length in a full-stack app?

Validate in one layer only — preferably the API/server layer. Send the character limit and current count to the frontend as hints, but enforce the limit server-side. Different languages count differently, so re-validating in multiple layers is a source of bugs, not safety.

What about grapheme clusters (like flag emoji)?

Flag emoji (🇺🇳) and skin-tone-modified emoji (👍🏽) are multiple code points that form a single visible character. Neither .length (JavaScript) nor len() (Python) counts them correctly. Use Intl.Segmenter in JavaScript or the grapheme library in Python for grapheme-aware counting.

Final Thoughts

String length seems like the simplest operation in programming until you deal with real-world text. Every language made a different design choice, and none of them are wrong — they just measure different things.

JavaScript measures UTF-16 code units. Python measures Unicode code points. Go measures bytes by default but gives you tools to measure runes. MySQL measures bytes. PostgreSQL measures characters.

The bug I described at the start of this article — the user bio that could not fit despite being 200 characters — happened because I assumed every layer measured the same thing. They do not. The fix was to validate on the server side only and give the frontend a hint, not an authoritative count.

Try the String Length Calculator tool on DevFormatters to see how different encodings and languages count your text differently.