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 \n escape 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 String wrapper object so property/method lookup can proceed via the prototype chain.
  • It resolves length (or, for a method call, looks up the method like toUpperCase on String.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(), and endsWith() instead of checking indexOf() !== -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) or parseInt/parseFloat before 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 .length counts 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-in reverse() method that strings lack.)
  • Exercise 2: Write a function isPalindrome(str) that returns true if 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, and padStart rather than manual loops for common text tasks.