JavaScript lesson 4 of 9
JavaScript Strings
Learn how JavaScript strings are created, why they are immutable, how indexing and slicing work, and the string methods you will use most, including padStart and padEnd for alignment.
Published · Every example on this page was run before it was published.
Almost every program you write will handle text at some point — a name typed into a form, a line read from a file, a message shown on the screen. In JavaScript, text lives in a value called a string, and strings come with their own rules and their own toolbox of built-in methods. This lesson walks through all of it, from the quotes you type to the alignment tricks that make output look professional.
What Is a String?
A string is a sequence of characters treated as a single value. You create one by wrapping characters
in quotes, and JavaScript gives you three ways to do it: single quotes, double quotes, or backticks.
Single and double quotes behave identically. Backticks create a template literal, which additionally
lets you embed expressions with ${} — you met this briefly in the lesson on printing, and this
lesson uses it throughout.
const single = 'Python';
const double = "Python";
console.log(single);
console.log(double);
console.log(single === double);Python
Python
trueHaving more than one quote style is genuinely useful, because it lets you put one kind of quote inside a string delimited by another kind without any extra work — a double-quoted string can contain an apostrophe freely, and a single-quoted one can contain a straight double quote.
Strings Are Immutable
This is the single most important fact about JavaScript strings: they are immutable, meaning that once a string exists, its characters can never be changed. You can build new strings out of old ones all day long, but you cannot edit one in place.
Reading a character is fine. You do it with square brackets and a position, called an index, counting from 0:
const language = "JavaScript";
console.log(language[0]);
console.log(language[3]);
console.log(language.length);J
a
10Two things behave differently here than you might expect. Asking for a position past the end of the
string does not raise an error — it simply gives you undefined:
const language = "JavaScript";
console.log(language[20]);undefinedAnd square brackets do not understand negative positions the way some languages' indexing does.
language[-1] does not mean "the last character" — JavaScript treats -1 as a property name that
does not exist on the string, so you get undefined again. For that, use the .at() method instead,
which does understand negative positions:
const language = "JavaScript";
console.log(language[-1]);
console.log(language.at(-1));
console.log(language.at(-2));undefined
t
pWriting is where immutability shows itself directly: attempting language[0] = "j" throws a
TypeError, because the individual characters of a string are read-only. Every string method you meet
below follows from this one fact — a method that appears to change a string is always building and
returning a brand-new one, leaving the original untouched:
const greeting = "hello";
const louder = greeting.toUpperCase();
console.log(greeting);
console.log(louder);hello
HELLOgreeting is still lowercase because it was never touched. If you want a variable to hold the new
value, you must assign the result to it — reassigning a let variable is allowed even though editing a
string's characters is not.
Indexing and Slicing
Beyond single positions, .slice(start, end) pulls out a whole range of characters at once. It
includes the character at start and stops just before end, and either argument can be negative,
counting back from the end of the string. Leaving out end means "through the end":
const word = "JavaScript";
console.log(word.slice(0, 4));
console.log(word.slice(4));
console.log(word.slice(-6));Java
Script
Script.slice() always produces a new string and never disturbs the one you sliced. Combined with an
array method you will meet properly in a later lesson, slicing gives you the standard JavaScript trick
for reversing a string — split it into individual characters, reverse that list, and glue it back
together:
const original = "hello";
const reversed = original.split("").reverse().join("");
console.log(reversed);olleh.split("") breaks the string into an array of one-character strings, .reverse() flips their order,
and .join("") welds them back into one string with nothing between them. This exact combination —
str === str.split("").reverse().join("") — is the standard way to check whether a string reads the
same forwards and backwards.
The String Methods You Will Use Most
JavaScript ships dozens of string methods. These are the ones that earn their keep in everyday code.
Changing case. .toUpperCase() and .toLowerCase() return case-converted copies. Lowercasing both
sides before comparing is the standard way to compare text without caring about case:
const shout = "quiet please";
console.log(shout.toUpperCase());
console.log("HELLO".toLowerCase() === "hello");QUIET PLEASE
trueTrimming whitespace. .trim() removes whitespace — spaces, tabs, newlines — from both ends of a
string, leaving the middle untouched. .trimStart() and .trimEnd() trim only one side. Unlike some
of the methods above, .trim() only ever removes whitespace — it has no way to strip an arbitrary
custom character. JSON.stringify() below is a handy trick for displaying a string with its edges
visible, since it wraps the value in literal quote marks:
const raw = " ada@example.com\n";
console.log(JSON.stringify(raw.trim()));"ada@example.com"Splitting and joining. .split(separator) breaks a string into an array of smaller strings at
every occurrence of separator. .join(separator) — a method on arrays, not strings — does the exact
reverse, welding an array of strings back into one:
const sentence = "the quick brown fox";
console.log(sentence.split(" "));
const csvRow = "ada,lovelace,1815";
console.log(csvRow.split(","));
console.log(csvRow.split(",").join(" | "));[ 'the', 'quick', 'brown', 'fox' ]
[ 'ada', 'lovelace', '1815' ]
ada | lovelace | 1815Notice the quote marks around each word in the first two results but not the third. Logging an array of strings shows each string in quotes so you can see exactly where one ends and the next begins, but a single string logged on its own — like the final line, which is one joined string — prints as plain text with no quotes at all.
Replacing. .replace(old, newText) returns a copy with the first match of old swapped for
newText. If you want every match replaced, you need .replaceAll() instead — this is a genuine trap
if you assume .replace() replaces everything:
const line = "I like cats. Cats are great.";
console.log(line.replace("cats", "dogs"));
console.log(line.replaceAll(".", "!"));I like dogs. Cats are great.
I like cats! Cats are great!The capitalised Cats survived the first line because .replace() is case-sensitive and only touched
the single lowercase match. The second line shows .replaceAll() catching every period, not just the
first.
Checking the ends and the middle. .startsWith(), .endsWith(), and .includes() each answer a
yes-or-no question and return true or false:
const filename = "report_2026.csv";
console.log(filename.startsWith("report"));
console.log(filename.endsWith(".csv"));
console.log(filename.includes("2026"));true
true
trueSearching for a position. .indexOf() reports where a substring first appears, and returns -1
when it is nowhere to be found. There is no separate version that throws an error on a failed search —
-1 is the only signal you get, so you must check for it yourself:
console.log("Hello".indexOf("l"));
console.log("Hello".indexOf("z"));
console.log("Hello".includes("z"));2
-1
falseAligning Text with padStart and padEnd
.padStart(targetLength, padString) and .padEnd(targetLength, padString) grow a string up to a given
length by adding characters to its start or end. If padString is left out, JavaScript pads with plain
spaces. This is the standard way to line up numbers and labels into readable columns.
console.log("7".padStart(3, "0"));
console.log("42".padStart(5, "0"));
console.log("Sam".padEnd(8, "-") + "|");007
00042
Sam-----|"7".padStart(3, "0") adds two zeros to the front until the string reaches length 3, which is
exactly how you would zero-pad a number for display. "Sam".padEnd(8, "-") adds dashes to the end
until the string reaches length 8 — the trailing "|" in that example is not part of the padding, it
is only there so you can see exactly where the padded field ends.
Used together, the two line up a column of labels against a column of numbers, regardless of how long each label or number is:
console.log("apples".padEnd(8, ".") + String(3).padStart(4, "."));
console.log("kiwi".padEnd(8, ".") + String(12).padStart(4, "."));apples.....3
kiwi......12.padEnd(8, ".") pushes every label out to the same width regardless of how many letters it has, and
.padStart(4, ".") pushes every number's final digit to line up in the same column. Swap the "." for
" " in real output and the effect is a tidy, evenly spaced table built from nothing but two string
methods.
A Worked Example
Here is a small program that cleans up a messy sign-up name and derives a few useful pieces from it. It
combines .trim(), .split(), a for...of loop over the words that .split() produces,
.toUpperCase(), .slice(), and .replaceAll().
function initials(fullName) {
let result = "";
for (const part of fullName.split(" ")) {
result += part[0].toUpperCase() + ".";
}
return result;
}
const raw = " ada lovelace ";
const name = raw.trim();
const username = name.replaceAll(" ", ".");
console.log(`Cleaned name: "${name}"`);
console.log(`Initials: ${initials(name)}`);
console.log(`Suggested username: ${username}@example.com`);
console.log(`Display name: ${name[0].toUpperCase() + name.slice(1)}`);Cleaned name: "ada lovelace"
Initials: A.L.
Suggested username: ada.lovelace@example.com
Display name: Ada lovelaceWalking through it: raw.trim() removes the stray leading and trailing spaces first, so every
calculation after it works from a clean "ada lovelace" rather than fighting extra whitespace.
initials() splits that clean name on the space between words, giving ["ada", "lovelace"], then
walks each piece with for...of, taking its first character with part[0], uppercasing it, and
building up a result string one piece at a time — exactly the accumulator pattern you will meet
formally in the lesson on loops.
username uses .replaceAll(" ", ".") to turn the space between the two words into a dot, which is a
common way to derive a username from a display name. The final line combines two techniques at once:
name[0] reads the very first character with plain indexing, .toUpperCase() capitalises just that
one character, and .slice(1) grabs everything from the second character onward — added together with
+, the two pieces reconstruct the whole name with only its first letter capitalised.
Common Mistakes
Forgetting that a method returns a new string instead of changing the original
const name = "ada";
name.toUpperCase();
console.log(name);adaThe uppercase string really was built — it was just thrown away immediately, because nothing captured it. Assign the result back to a variable to keep it:
const name2 = "ada";
const upper = name2.toUpperCase();
console.log(upper);ADACalling a method on a result that turned out to be undefined
const color = "red";
console.log(color[10]);
console.log(color[10].toUpperCase());undefined
TypeError: Cannot read properties of undefined (reading 'toUpperCase')color[10] is past the end of a three-character string, so it evaluates to undefined rather than
raising an error on its own — you saw this earlier in the lesson. The crash only happens on the next
line, when the code tries to call .toUpperCase() on that undefined value, and undefined has no
methods at all. Whenever an error message mentions "Cannot read properties of undefined," look one step
earlier in the chain for the lookup that quietly produced undefined in the first place.
Treating indexOf's result as a plain yes-or-no answer
const text = "banana bread";
if (text.indexOf("banana")) {
console.log("found it, sort of");
} else {
console.log("not found?");
}not found?"banana bread" clearly contains "banana" — right at the very start, at index 0. That is exactly
the problem: 0 is a falsy value, so if (text.indexOf("banana")) treats a match at the beginning
of the string the same as no match at all. The fix is to compare the result against -1 explicitly, or
to reach for .includes(), which was built precisely to avoid this trap:
const text = "banana bread";
console.log(text.indexOf("banana") !== -1);
console.log(text.includes("banana"));true
trueNext Steps
The two practice problems linked with this lesson exercise exactly what you just read. Valid
Palindrome asks whether a string reads the same forward and backward, which is where
.toLowerCase() and the .split("").reverse().join("") trick come together. Reverse Words in a
String is a direct workout for .split(), array reversal, and .join().
Before jumping in, open the JavaScript playground and retype a few
examples from this lesson by hand — especially the slicing ones, changing the numbers to see what
comes out. Immutability and the .replace() versus .replaceAll() distinction are much easier to
internalise by experiment than by reading, and ten minutes of poking at strings in the playground will
save you a lot of debugging later.
When you want a harder one
Interview-style problems graded against hidden tests — a big step up from the exercises. Come back to these when the ideas in this lesson feel comfortable rather than new.