JavaScript Strings and Template Literals
AI-assistedDrafted with AI assistance and reviewed before publishing; examples are version-pinned. Verify against official docs when in doubt.
Almost every script touches strings: building output, slicing fields, cleaning input, generating markup. This chapter covers the methods that come up daily and the two or three semantics that trip people up.
Three kinds of quotes
const a = "double";
const b = 'single';
const c = `template literal`;
Double and single quotes are equivalent; pick whichever avoids escaping:
const text = 'He said "hello"';
const other = "It's fine";
Use backticks when you need interpolation or multiple lines. With plain quotes, a newline requires \n.
Template literals
Anything inside ${} is an expression:
const name = "ann";
const score = 95;
console.log(`${name} scored ${score}`); // ann scored 95
console.log(`passed: ${score >= 60}`); // passed: true
console.log(`$${(19.99 * 1.13).toFixed(2)}`); // computed inline
Multi-line text needs no escaping, and indentation is preserved:
const html = `
<ul>
<li>${name}</li>
</ul>
`;
Nesting template literals inside ${} is legal but hurts readability; extract a variable beyond one level.
Strings are immutable
let s = "hello";
s[0] = "H";
console.log(s); // "hello"
The assignment silently does nothing (strict mode throws TypeError). Every “modifying” method returns a new string:
const s = "hello";
s.toUpperCase(); // "HELLO", discarded
const upper = s.toUpperCase(); // what you meant
The same applies to trim, replace, padStart, and the rest. String methods never mutate.
Length and indexing
const s = "hello";
s.length; // 5
s[0]; // "h"
s.at(-1); // "o" (ES2022, negative indexing)
length counts UTF-16 code units, not characters:
"😀".length; // 2
[... "😀"].length; // 1
If you need user-perceived characters, convert with spread or Array.from(str) and work on the array. For grapheme clusters (emoji with modifiers, flags), use Intl.Segmenter.
Searching and testing
const s = "hello world";
s.includes("world"); // true
s.startsWith("he"); // true
s.endsWith("ld"); // true
s.indexOf("o"); // 4, or -1
s.lastIndexOf("o"); // 7
includes answers existence; indexOf gives the position. Case-insensitive checks need normalization:
s.toLowerCase().includes("WORLD".toLowerCase());
Slicing: slice versus substring
const s = "hello world";
s.slice(0, 5); // "hello"
s.slice(6); // "world"
s.slice(-5); // "world"
s.substring(0, 5); // "hello"
Differences:
sliceaccepts negative indices;substringtreats them as0.slicereturns""whenstart > end;substringswaps the arguments.sliceis also the array method, so one habit covers both.
Use slice.
Replacing
const s = "a-b-c";
s.replace("-", "/"); // "a/b-c" first match only
s.replace(/-/g, "/"); // "a/b/c" regex with global flag
s.replaceAll("-", "/"); // "a/b/c" ES2021
replace with a string argument changes only the first occurrence. This is the most common string bug in review. Use replaceAll or a /g regex.
In the replacement string, $&, $1, and $$ have special meanings. When you need a literal, pass a function:
s.replaceAll("-", () => "/");
Split and join
"a,b,c".split(","); // ["a", "b", "c"]
"a,b,c".split(",", 2); // ["a", "b"]
["a", "b"].join("-"); // "a-b"
["a", "b"].join(""); // "ab"
They are inverses, and together they cover most delimited-text work. For real CSV, use a parser: fields can contain the delimiter inside quotes.
Trimming and padding
" hi ".trim(); // "hi"
" hi ".trimStart(); // "hi "
" hi ".trimEnd(); // " hi"
"7".padStart(3, "0"); // "007"
"7".padEnd(3, "-"); // "7--"
trim removes spaces, tabs, and newlines at both ends. On user input it is almost always the first step, before validation.
Case and repetition
"Hello".toLowerCase(); // "hello"
"Hello".toUpperCase(); // "HELLO"
"-".repeat(3); // "---"
Case-insensitive comparison is a normalize-then-compare:
const same = a.trim().toLowerCase() === b.trim().toLowerCase();
For locale-sensitive comparison, prefer localeCompare or Intl.Collator.
Worked example: normalizing a tag
function normalizeTag(raw) {
return raw
.trim()
.toLowerCase()
.replaceAll(/\s+/g, "-")
.replaceAll(/[^a-z0-9-]/g, "");
}
normalizeTag(" Hello World "); // "hello-world"
normalizeTag("Node.js 22"); // "nodejs22"
The chain is readable because each step is a single-purpose method returning a new string.
Common mistakes
1. replace used for a global change
Only the first match changes. Use replaceAll or /g.
2. Treating length as a character count
Emoji and some CJK-adjacent characters take two code units. Spread the string when counting user-visible characters.
3. Trying to assign to str[0]
Strings are immutable; build a new string instead.
4. Negative indices with substring
They become 0. Use slice.
5. Building long strings with += in a hot loop
Each concatenation creates a new string. Collect parts in an array and join("").
6. Over-nesting template literals
Pull the inner part into a named variable; the resulting code is easier to review.
Summary
- Template literals interpolate expressions and span lines.
- Strings are immutable; methods return new values.
includesfor existence,slicefor ranges,replaceAllfor global replacement.splitandjoinhandle delimited text.lengthcounts code units, not characters.
Next: throwing, catching, and propagating errors.