Strings and text processing · Lesson 36 of 77 · concept
Strings cannot be changed, only rebuilt
About 45 minutes.
By the end of this lesson you can
- Say what a string is: a fixed, ordered run of characters you can read but never edit in place.
- Read one character out of a string with square brackets and with .at(), counting from either end.
- Explain why assigning to text[0] changes nothing, and what to do instead.
- Rebuild a changed copy of a string out of pieces of the original.
- Spot the 'I called .toUpperCase() and nothing happened' bug and fix it.
What a string actually is
Explanation
A string is a piece of text: a name, a postcode, a whole email, a single letter. You write one by putting characters between quotes — "hello", 'hello' and `hello` are all the same string, three ways of spelling it.
The important word is ordered. A string is not a bag of letters, it is a sequence: first character, second character, third, and so on, each one at a numbered position. Those positions start at 0, not 1. In "hello", the h is at position 0 and the o is at position 4.
The second important word is fixed. Once a string exists, nothing can change it. Not you, not a method, not a loop. Every operation that looks like it edits text actually makes a brand-new string and leaves the old one exactly as it was. That single fact explains most of the confusion beginners have with text, so the whole lesson is built on it.
A printed page, not a whiteboard
An analogy
Think of a string as a page that has already come out of the printer. You can read it, photocopy it, cut a paragraph out of the copy, staple two copies together — but you cannot make the ink on the original page rearrange itself.
A variable, meanwhile, is a hook on the wall that holds one page at a time. When you 'change' text, what really happens is that you print a new page and hang it on the hook instead. The old page is untouched; it is just no longer the one on the hook.
This is why `let` and `const` matter here and are not the same question. `const name = "ada"` means you cannot move a different page onto that hook. It does not mean the page is protected — the page was always unchangeable. Strings are immutable no matter which keyword you used.
Reading one character at a time
Explanation
Square brackets read a character by position: `title[0]` is the first character. Because positions start at 0, the LAST character is at `title.length - 1`, which is a small piece of arithmetic you will type a thousand times.
`.length` tells you how many characters are in the string. It is a property, not a method, so there are no brackets after it: `title.length`, never `title.length()`.
There is also `.at()`, added to the language in 2022. `title.at(0)` does the same as `title[0]`, but `.at()` accepts negative positions that count back from the end: `title.at(-1)` is the last character and `title.at(-2)` is the one before it. Square brackets do NOT do this — `title[-1]` is always `undefined`, because -1 is simply not a position in the string.
Asking for a position that does not exist is not an error. Both `title[99]` and `title.at(99)` quietly hand back `undefined`. That is worth remembering, because a typo in your arithmetic will not announce itself; it will just produce `undefined` and travel onward through your program.
Every string method hands you a new string
A mental model
`.toUpperCase()`, `.trim()`, `.slice()`, `.replace()` — every one of them RETURNS a new string and leaves the one you called it on completely alone. None of them changes anything.
So the rule is: if you call a string method and do not keep what it gives back, you have done nothing at all. `name.toUpperCase();` on its own is a line of code with no effect whatsoever. You need `const shouted = name.toUpperCase();` or `name = name.toUpperCase();` — you have to catch the new page.
This is the opposite of how arrays behave, which is why the two feel inconsistent. `list.push("x")` really does change `list`. `text.toUpperCase()` really does not change `text`. Arrays are mutable, strings are not; when you move between them, expect to be caught out at least once.
The three ways this bites people
A common mistake
First: `name[0] = "A"`. This looks like it should work, and it does not throw an error either — the assignment is simply ignored and `name` is unchanged. (In a file running under strict mode it throws a TypeError instead. Either way you do not get the edit you wanted.)
Second: calling a method and dropping the result, as above. The giveaway is a line that consists only of `something.someMethod();` where the method is a string method.
Third: expecting `.replace()` to change the variable. `text.replace("a", "b")` gives you back a new string with the swap made; `text` still says exactly what it said before. Assign the result somewhere or it is gone.
The fix for all three is identical: build the string you want, and put it in a variable. There is no in-place edit to reach for, so stop looking for one.
What to take away
Recap
A string is an ordered, numbered, unchangeable run of characters. Read with `[index]` or `.at(index)`; count with `.length`; positions start at 0 and `.at()` accepts negatives.
You never edit text. You build a new piece of text out of the old one — usually with `+` or with a slice of the original on each side of the bit you are changing — and you keep the result.
Worked examples
Every output below was produced by actually running the code in the same sandbox your exercises run in — it is not a prediction.
Reading characters and counting them
Every way of getting at a single character, including the two ways of asking for a position that does not exist. Watch what happens on the last two lines rather than skipping them — silent undefined is the failure mode you will meet in real code.
1const title = "Novus";2console.log(title.length);3console.log(title[0]);4console.log(title.at(-1));5console.log(title[99]);6console.log(title.at(99));- line 2
A property, not a method: no brackets after length. Five characters, so length is 5 and the last valid position is 4.
- line 3
Position 0 is the FIRST character, not the second.
- line 4
at(-1) counts back from the end, so this is the last character. title[-1] would be undefined instead.
- line 5
Position 99 does not exist. No error, no warning — just undefined.
What it prints
5Nsundefinedundefined
The edit that silently does not happen
Two failed attempts at changing a string, one after the other, and then the version that works. This is the single most useful example in the lesson: run it, and the immutability rule stops being an abstract claim.
1const name = "ada";2name[0] = "A";3console.log(name);4name.toUpperCase();5console.log(name);6const shouted = name.toUpperCase();7console.log(shouted);- line 2
Looks like an edit. Is not an edit. The line runs, nothing changes, no error is raised here.
- line 3
Still lowercase, exactly as it was declared.
- line 4
The uppercase string really was created — and then thrown away, because nothing caught it.
- line 6
This is the only line in the example that achieves anything: it keeps the new string.
What it prints
adaadaADA
Rebuilding text instead of editing it
How you 'change' a character when changing is impossible: take the part before it, the part after it, and glue your replacement in the middle. This three-piece pattern is exactly what the exercise asks you to write.
1const original = "hello world";2const fixed = original.slice(0, 6) + "W" + original.slice(7);3console.log(original);4console.log(fixed);5console.log(original === fixed);- line 2
slice(0, 6) is everything up to but NOT including position 6 — 'hello '. slice(7) is everything from position 7 onwards — 'orld'. Position 6 (the old w) is the one being skipped.
- line 3
The original is untouched, as it always is. Nothing you can write would touch it.
- line 5
Two different strings, so === is false. You made a new page rather than editing the old one.
What it prints
hello worldhello Worldfalse
Check yourself
Not scored, not stored. Getting one wrong is the useful part.
Read this carefully before you run it. Two of these three lines look like they change the word. Write down what you think each console.log prints, then check.
const word = "star";word[0] = "c";console.log(word);console.log(word.toUpperCase());console.log(word);
You have `const code = "A7X";` and you want the last character, "X", without knowing the length in advance. Which expression gives it to you?
Which sentence is true of every string method in JavaScript?
Write it yourself
Exercise · intro · 7 tests
Swap one character, without editing anything
Write a function `replaceCharAt(text, index, replacement)` that gives back a NEW string, identical to `text` except that the character at position `index` has been replaced by `replacement`. Because strings cannot be edited, you have to build the answer out of three pieces: the part of `text` before the index, then `replacement`, then the part of `text` after the index. `text.slice(0, index)` and `text.slice(index + 1)` give you the two ends — mind the `+ 1`, because you are skipping the character you are replacing. If `index` is not a real position in the string (negative, or equal to or past the length), give back `text` unchanged rather than producing something odd.
For any text, any index and any single-character replacement, return a new string identical to text apart from the one character at index. When index is below 0 or is text.length or more, return text exactly as it was given. The string passed in must be unchanged afterwards.
Define replaceCharAt at the top level of your code — the tests call it by name.
Worth remembering
- Can you change a character inside an existing string?
No. Strings are immutable. text[0] = "A" is silently ignored (and throws in strict mode). You build a new string — usually text.slice(0, i) + newChar + text.slice(i + 1) — and keep the result.
- What does name.toUpperCase(); on a line by itself accomplish?
Nothing. Every string method returns a NEW string and leaves the original alone, so a result you do not store is thrown away. You need const shouted = name.toUpperCase().
- How do you read the last character of a string without knowing its length?
text.at(-1). Negative positions count back from the end. Square brackets do not: text[-1] is always undefined.
Check this lesson against the source
We wrote the explanation; we did not invent the facts. This is the page that backs them.
Check there: The 'Character access' section states that strings are immutable and that assigning to an index has no effect (and throws in strict mode); it also documents length and bracket access.
Check there: at() accepts a negative index counting back from the end and returns undefined when the index is out of range.
Check there: length is a read-only property holding the number of UTF-16 code units in the string.
Prefer to read the source in a structured breakdown? Turn this doc into a breakdown