JavaScript Strings
A string in JavaScript is a sequence of characters used to represent text — names, sentences, JSON data, HTML markup, you name it. Strings are one of the seven primitive types in JavaScript, and because nearly every program manipulates text in some way (reading input, building messages, parsing data), mastering strings is one of the highest-leverage skills you can build early on. This lesson covers how strings actually work under the hood, every way to create and manipulate them, and the mistakes that trip up even experienced developers.
Overview: How Strings Work
A JavaScript string is a primitive value — like a number or a boolean, it is not an object, and it is immutable. Once a string is created, its contents can never be changed. Every method you call on a string that appears to “modify” it — toUpperCase(), trim(), replace() — actually returns a brand-new string, leaving the original untouched. If you assign the result back to the same variable, it looks like the string changed, but really the variable now just points to a different string value in memory.
Internally, JavaScript strings are encoded as sequences of UTF-16 code units. Most common characters (basic Latin letters, digits, punctuation) fit into a single 16-bit code unit, so "cat".length is simply 3. But characters outside the Basic Multilingual Plane — many emoji, for example — are represented as a surrogate pair: two 16-bit code units that together represent one visual character. This is why "😀".length returns 2 instead of 1, and it’s a common source of bugs when slicing strings that contain emoji or rare scripts.
Even though strings are primitives, you can call methods on them, like "hello".toUpperCase(). This works because of a process called auto-boxing: when you access a property or method on a string primitive, the JavaScript engine momentarily wraps it in a temporary String wrapper object, looks up the method on that object (and its prototype chain), runs it, and then discards the wrapper. You never see this wrapper object directly, and you should almost never create one yourself with new String("x") — doing so creates an actual object, not a primitive, which behaves surprisingly in comparisons (an object is never === or even loosely == equal to a primitive string of the same text in certain contexts, and it is always truthy even for an empty string).
Syntax
There are three ways to write a string literal in JavaScript:
| Syntax | Example | Notes |
|---|---|---|
| Single quotes | 'hello' |
No functional difference from double quotes |
| Double quotes | "hello" |
No functional difference from single quotes |
| Template literals (backticks) | `hello` |
Support interpolation and multi-line text |
Single and double quotes behave identically — pick one convention and stay consistent (most modern style guides prefer double quotes for prose-like strings, but plenty of teams use single quotes). Template literals, introduced in ES6, use backticks and unlock two extra powers that quoted strings don’t have:
- Interpolation — embed any expression directly inside
${ }, e.g.`Hello, ${name}!` - Multi-line strings — a template literal can span multiple lines without needing
\nescape sequences or string concatenation
Common escape sequences work inside single- and double-quoted strings (and inside template literals too): \n (newline), \t (tab), \\ (backslash), \' and \" (quote characters), and \u{1F600} for a Unicode code point by number.
Examples
Example 1: Creating and inspecting strings
const firstName = "Ada";
const lastName = 'Lovelace';
const fullName = `${firstName} ${lastName}`;
console.log(fullName);
console.log(fullName.length);
console.log(typeof fullName);
Output:
Ada Lovelace
12
string
This example shows all three literal syntaxes in one place. The template literal `${firstName} ${lastName}` interpolates both variables directly into the string, which is far more readable than concatenating with +. Note that typeof reports "string" — confirming that even though we called no constructor, the result is a plain primitive.
Example 2: Searching and transforming text
const sentence = " JavaScript is fun to learn! ";
const trimmed = sentence.trim();
const upper = trimmed.toUpperCase();
const wordIndex = trimmed.indexOf("fun");
const excerpt = trimmed.slice(0, 10);
console.log(trimmed);
console.log(upper);
console.log(wordIndex);
console.log(excerpt);
console.log(trimmed.includes("learn"));
Output:
JavaScript is fun to learn!
JAVASCRIPT IS FUN TO LEARN!
14
JavaScript
true
trim() strips leading and trailing whitespace (but not whitespace in the middle). indexOf() returns the zero-based position where a substring first appears, or -1 if it’s not found — here "fun" starts at index 14. slice(0, 10) extracts characters from index 0 up to (but not including) index 10. includes() is the modern, readable way to check for a substring’s presence and returns a plain boolean instead of an index you’d have to compare against -1.
Example 3: A realistic formatting task
function formatPrice(cents) {
const dollars = (cents / 100).toFixed(2);
return `$${dollars}`;
}
const items = ["apple", "banana", "cherry"];
const receipt = items.map((item, i) => `${i + 1}. ${item}`).join("\n");
console.log(receipt);
console.log(formatPrice(1999));
const orderId = "42";
console.log(orderId.padStart(6, "0"));
let name = "javascript";
name[0] = "J";
console.log(name);
Output:
1. apple
2. banana
3. cherry
$19.99
000042
javascript
This ties several ideas together. map() builds an array of formatted lines using a template literal, and join("\n") stitches them into one multi-line string. padStart(6, "0") pads "42" with leading zeros until it’s 6 characters long — handy for order numbers, invoice IDs, or zero-padded timestamps. Finally, notice that name[0] = "J" has no effect: strings are immutable, so index assignment silently fails (in non-strict mode) and name still logs as "javascript".
Under the Hood: Step by Step
When the engine encounters fullName.length from Example 1, here’s roughly what happens:
- The engine sees you’re accessing a property (
.length) on a value that is a string primitive, not an object. - It temporarily boxes the primitive into a hidden
Stringwrapper object so property/method lookup can proceed via the prototype chain. - It resolves
length(or, for a method call, looks up the method liketoUpperCaseonString.prototype). - The method runs, reading the string’s internal UTF-16 code unit sequence, and returns a new primitive value — it never mutates the original.
- The temporary wrapper object is discarded; you’re left holding a fresh primitive.
This is also why strings are safe to share freely between functions: since nothing can mutate a string in place, passing a string into a function can never let that function corrupt your original data, unlike passing a mutable object or array by reference.
Quick Method Reference
| Method | Purpose |
|---|---|
length |
Number of UTF-16 code units |
slice(start, end) |
Extract a substring (supports negative indexes) |
indexOf(str) / lastIndexOf(str) |
Find position of a substring |
includes(str) / startsWith(str) / endsWith(str) |
Boolean substring checks |
toUpperCase() / toLowerCase() |
Change letter case (new string) |
trim() / trimStart() / trimEnd() |
Remove whitespace |
split(separator) |
Turn a string into an array |
replace(pattern, replacement) / replaceAll(...) |
Substitute text |
padStart(len, pad) / padEnd(len, pad) |
Pad to a fixed length |
repeat(n) |
Repeat a string n times |
Common Mistakes
Mistake 1: Trying to mutate a string by index
Because array-like index access (str[0]) works for reading characters, beginners often assume it also works for writing:
let word = "cat";
word[0] = "b";
console.log(word); // still "cat"
This doesn’t throw an error in non-strict mode — it just silently does nothing, which is even more dangerous because the bug goes unnoticed. Strings are immutable, so there is no in-place edit. To “change” a string, build a new one and reassign the variable:
let word = "cat";
word = "b" + word.slice(1);
console.log(word);
Output:
bat
Mistake 2: Assuming + always adds numbers
The + operator is overloaded: if either operand is a string, it performs string concatenation instead of numeric addition. This causes bugs whenever numeric-looking data actually arrives as text (which is extremely common — form inputs, URL query params, and JSON fields are frequently strings):
const price = "10";
const quantity = 3;
console.log(price * quantity);
console.log(price + quantity);
Output:
30
103
* forces numeric coercion on both operands, giving 30. But + sees a string operand and concatenates, giving the surprising "103". The fix is to explicitly convert with Number(price) or the unary +price before adding.
Best Practices
- Prefer template literals for any string that involves interpolation or spans multiple lines — they’re more readable than
+concatenation. - Never use
new String("x")to create a string; it produces an object wrapper with confusing equality behavior. Use the literal form instead. - Use
includes(),startsWith(), andendsWith()instead of checkingindexOf() !== -1— they’re clearer and self-documenting. - When comparing user input, normalize case and whitespace first (e.g.
input.trim().toLowerCase()) to avoid subtle equality bugs. - Convert numeric strings explicitly with
Number(str)orparseInt/parseFloatbefore doing arithmetic — don’t rely on implicit coercion. - Use
replaceAll()(ES2021+) instead of a global regex when you just need to replace every literal occurrence of a substring. - Remember that
.lengthcounts UTF-16 code units, not visual characters — be cautious slicing strings that may contain emoji or non-Latin scripts.
Practice Exercises
- Exercise 1: Write a function
reverseString(str)that returns the input string reversed. (Hint: convert it to an array first, since arrays have a built-inreverse()method that strings lack.) - Exercise 2: Write a function
isPalindrome(str)that returnstrueif a string reads the same forwards and backwards, ignoring case and spaces. Test it with"Race car". - Exercise 3: Given
const csv = "name,age,city";, write code that splits it into an array of fields and then joins them back together separated by" | "instead of commas. Expected output:"name | age | city".
Summary
- Strings are immutable primitives — methods that appear to modify a string actually return a new one.
- There are three literal syntaxes: single quotes, double quotes, and template literals (backticks), with template literals supporting interpolation and multi-line text.
- Strings are internally sequences of UTF-16 code units, so characters outside the Basic Multilingual Plane (like many emoji) can span two code units.
- Calling a method on a string primitive works via temporary auto-boxing into a hidden wrapper object.
- The
+operator concatenates when either operand is a string, which is a frequent source of bugs with numeric-looking string data. - Use built-in methods like
slice,includes,split,trim, andpadStartrather than manual loops for common text tasks.
