Skip to content

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.

JavaScript
const single = 'Python';
const double = "Python";
console.log(single);
console.log(double);
console.log(single === double);
Output
Python
Python
true

Having 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:

JavaScript
const language = "JavaScript";
console.log(language[0]);
console.log(language[3]);
console.log(language.length);
Output
J
a
10

Two 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:

JavaScript
const language = "JavaScript";
console.log(language[20]);
Output
undefined

And 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:

JavaScript
const language = "JavaScript";
console.log(language[-1]);
console.log(language.at(-1));
console.log(language.at(-2));
Output
undefined
t
p

Writing 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:

JavaScript
const greeting = "hello";
const louder = greeting.toUpperCase();
console.log(greeting);
console.log(louder);
Output
hello
HELLO

greeting 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":

JavaScript
const word = "JavaScript";
console.log(word.slice(0, 4));
console.log(word.slice(4));
console.log(word.slice(-6));
Output
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:

JavaScript
const original = "hello";
const reversed = original.split("").reverse().join("");
console.log(reversed);
Output
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:

JavaScript
const shout = "quiet please";
console.log(shout.toUpperCase());
console.log("HELLO".toLowerCase() === "hello");
Output
QUIET PLEASE
true

Trimming 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:

JavaScript
const raw = "   ada@example.com\n";
console.log(JSON.stringify(raw.trim()));
Output
"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:

JavaScript
const sentence = "the quick brown fox";
console.log(sentence.split(" "));
const csvRow = "ada,lovelace,1815";
console.log(csvRow.split(","));
console.log(csvRow.split(",").join(" | "));
Output
[ 'the', 'quick', 'brown', 'fox' ]
[ 'ada', 'lovelace', '1815' ]
ada | lovelace | 1815

Notice 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:

JavaScript
const line = "I like cats. Cats are great.";
console.log(line.replace("cats", "dogs"));
console.log(line.replaceAll(".", "!"));
Output
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:

JavaScript
const filename = "report_2026.csv";
console.log(filename.startsWith("report"));
console.log(filename.endsWith(".csv"));
console.log(filename.includes("2026"));
Output
true
true
true

Searching 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:

JavaScript
console.log("Hello".indexOf("l"));
console.log("Hello".indexOf("z"));
console.log("Hello".includes("z"));
Output
2
-1
false

Aligning 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.

JavaScript
console.log("7".padStart(3, "0"));
console.log("42".padStart(5, "0"));
console.log("Sam".padEnd(8, "-") + "|");
Output
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:

JavaScript
console.log("apples".padEnd(8, ".") + String(3).padStart(4, "."));
console.log("kiwi".padEnd(8, ".") + String(12).padStart(4, "."));
Output
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().

JavaScript
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)}`);
Output
Cleaned name: "ada lovelace"
Initials: A.L.
Suggested username: ada.lovelace@example.com
Display name: Ada lovelace

Walking 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

JavaScript
const name = "ada";
name.toUpperCase();
console.log(name);
Output
ada

The 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:

JavaScript
const name2 = "ada";
const upper = name2.toUpperCase();
console.log(upper);
Output
ADA

Calling a method on a result that turned out to be undefined

JavaScript
const color = "red";
console.log(color[10]);
console.log(color[10].toUpperCase());
Output
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

JavaScript
const text = "banana bread";
if (text.indexOf("banana")) {
  console.log("found it, sort of");
} else {
  console.log("not found?");
}
Output
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:

JavaScript
const text = "banana bread";
console.log(text.indexOf("banana") !== -1);
console.log(text.includes("banana"));
Output
true
true

Next 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.