Strings

JavaScript keeps all text in strings. There’s no distinct type for a single character the way C has char — one letter is just a string of length one.

Internally, every string is stored as UTF-16, no matter how the surrounding page is encoded. UTF-16 means each string is a sequence of 16-bit numbers under the hood, and most characters you type map to exactly one of those units. A handful of characters (emoji, some rarer scripts) take two units, and that’s the source of a few surprises covered later and in the Unicode chapter. For everyday Latin text you can picture a string as a plain row of characters.

Quotes

Three characters can wrap a string: single quotes, double quotes, and backticks.

let single = 'single-quoted';
let double = "double-quoted";

let backticks = `backticks`;

Single and double quotes do the same job — pick whichever keeps the fewest escapes. Backticks give you two extra powers that the other two lack.

‘single’
interpolation  no
line breaks  no
“double”
interpolation  no
line breaks  no
`backtick`
interpolation  yes
line breaks  yes
The three ways to delimit a string. Single and double quotes are interchangeable; backticks add interpolation and real line breaks.

The first power is interpolation. Inside a backtick string, wrap any expression in ${…} and JavaScript evaluates it, converts the result to a string, and drops it in place:

function total(a, b) {
  return a + b;
}

alert(`4 + 5 = ${total(4, 5)}.`); // 4 + 5 = 9.

The expression can be anything: a variable, arithmetic, a function call, a property lookup. Whatever value comes back is coerced to text using the usual string conversion rules.

`4 + 5 = ${total(4, 5)}.`

evaluate the expression, convert to string
“4 + 5 = 9.”
Interpolation evaluates each slot, coerces the result to a string, and stitches the pieces back into one string.

Try it live. Type a name and a number of guests; the greeting is built with a single backtick string that interpolates both values and a small calculation:

interactiveInterpolation builds a string from live values

The second power is real line breaks. A backtick string can span several lines and keep every break exactly as written:

let guestList = `Guests:
 * Maya
 * Raj
 * Lena
`;

alert(guestList); // a list of guests, spread over multiple lines

The single- and double-quoted versions can’t do that. Break the line and the parser reports an error at the point where the string was left hanging:

let guestList = "Guests: // Error: Unexpected token ILLEGAL
  * Maya";

Single and double quotes date back to the earliest days of the language, when multiline strings weren’t a concern. Backticks arrived much later, which is why they’re the more capable of the three.

Backticks have one more trick that’s easy to miss. You can place a function name right before the opening backtick — the syntax is func`string`. That function, func, runs automatically and receives the string pieces and the interpolated values as separate arguments, so it can process them however it likes. The feature is called “tagged templates.” You rarely need it, but it powers tools like styled-components and safe HTML builders. The details live on MDN under Template literals.

Special characters

Even single- and double-quoted strings can hold line breaks, using the newline character written as \n:

let guestList = "Guests:\n * Maya\n * Raj\n * Lena";

alert(guestList); // a multiline list of guests, same as above

To see that a plain \n and a real line break produce the same thing, compare the two forms directly:

let str1 = "Sea\nSky"; // two lines using a newline symbol

// two lines using an actual line break inside backticks
let str2 = `Sea
Sky`;

alert(str1 == str2); // true

\n is one of several backslash sequences. Here are the ones worth knowing:

Character Description
\n Line break.
\r Carriage return. Windows text files end each line with the pair \r\n; other systems use a lone \n. It’s historical baggage, and most Windows software accepts a bare \n anyway.
\'\"\` A quote that matches the surrounding quotes, inserted as a literal character.
\\ A single literal backslash.
\t Tab.
\b, \f, \v Backspace, form feed, vertical tab. Listed for completeness; they come from the typewriter era and you’ll never reach for them in practice.

Every one of these starts with a backslash \, which is why the backslash is called the “escape character.”

Because the backslash is special, showing a literal one means writing two in a row:

alert( `The backslash: \\` ); // The backslash: \

The escaped quotes \', \", and \` let you place a quote inside a string wrapped in that same quote:

alert( 'We\'re open!' ); // We're open!

Without the backslash, that inner ' would close the string early, so it has to be escaped as \'.

You only need to escape the quote that matches the wrapper. A cleaner move is to switch the wrapper to a quote the text doesn’t use:

alert( "We're open!" ); // We're open!

There’s also a \u… notation for Unicode code points. It shows up rarely and gets its own treatment in the optional Unicode chapter.

String length

The length property holds the character count:

alert( `Go\n`.length ); // 3

\n is a single character, so the count is 3, not 4.

G
o
\n
→  length 3
The word Go followed by a newline is three characters. The backslash-n pair counts as one.

Accessing characters

To read the character at position pos, use square brackets [pos] or call str.at(pos). Positions start at zero:

let str = `Coral`;

// the first character
alert( str[0] ); // C
alert( str.at(0) ); // C

// the last character
alert( str[str.length - 1] ); // l
alert( str.at(-1) ); // l

at has a benefit the brackets don’t: a negative position counts back from the end. So .at(-1) is the last character, .at(-2) the one before it, and so on.

0
C
-5
1
o
-4
2
r
-3
3
a
-2
4
l
-1
Every character sits at a position. Brackets and .at() count from 0 on the left; .at() also takes negative positions that count from the right.

The brackets have no such convenience. Feed them a negative index and you get undefined:

let str = `Coral`;

alert( str[-2] ); // undefined
alert( str.at(-2) ); // a

You can also walk a string character by character with for..of:

for (let char of "Coral") {
  alert(char); // C,o,r,a,l (char is "C", then "o", then "r", and so on)
}

Drag the slider to pick a position and watch both [pos] and .at(pos) respond. Negative positions show off .at’s trick — the brackets just return undefined:

interactiveBracket index vs .at() with negative positions

Strings are immutable

A string, once made, can’t be changed. There’s no way to overwrite a character in place.

Here’s the attempt that proves it:

let str = 'Yo';

str[0] = 'y'; // error
alert( str[0] ); // doesn't work

The way around it is to build a whole new string and assign it back over the old variable:

let str = 'Yo';

str = 'y' + str[1]; // replace the whole string

alert( str ); // yo
str[0] = ‘y’  ✗
Y
o
characters are read-only
str = ‘y’ + str[1]  ✓
y
o
a brand-new string, rebound to str
You can't overwrite one character in place. The fix is to build a fresh string and rebind the variable to it.

This pattern — throw away the old string, keep the new one — shows up in most of the methods below. They never touch the original; they hand back a new string for you to use.

Changing the case

toLowerCase() and toUpperCase() flip the case of every letter:

alert( 'Keyboard'.toUpperCase() ); // KEYBOARD
alert( 'Keyboard'.toLowerCase() ); // keyboard

Want just one character in a different case? Index into the string first, then call the method on that single-character result:

alert( 'Keyboard'[0].toLowerCase() ); // 'k'

Searching for a substring

There’s more than one way to find a piece of text inside a string.

str.indexOf

The workhorse is str.indexOf(substr, pos).

It scans str for substr, beginning at position pos, and returns the position of the first match. If there’s no match at all, it returns -1.

let str = 'Bonfire beacon';

alert( str.indexOf('Bonfire') ); // 0, 'Bonfire' sits at the very start
alert( str.indexOf('bonfire') ); // -1, no match — the search is case-sensitive

alert( str.indexOf("on") ); // 1, "on" is found at position 1 (Bon...)

The optional second argument sets where the search starts. The first "on" is at position 1; to find the next one, start scanning from position 2:

let str = 'Bonfire beacon';

alert( str.indexOf('on', 2) ) // 12
0
B
1
o
2
n
3
f
4
i
5
r
6
e
7
·
8
b
9
e
10
a
11
c
12
o
13
n
indexOf(“on”) → 1   indexOf(“on”, 2) → 12
indexOf returns the position of the first match at or after its start position. Searching again past that position finds the next one.

To visit every occurrence, run indexOf in a loop, each time starting just past the previous hit:

let str = 'A bat, a cat, and a rat';

let target = 'at'; // the substring to look for

let pos = 0;
while (true) {
  let foundPos = str.indexOf(target, pos);
  if (foundPos == -1) break;

  alert( `Found at ${foundPos}` );
  pos = foundPos + 1; // resume from the character after this match
}

The same loop can be folded into a tighter form, doing the search and the test in one line:

let str = "A bat, a cat, and a rat";
let target = "at";

let pos = -1;
while ((pos = str.indexOf(target, pos + 1)) != -1) {
  alert( pos );
}

One rough edge trips up nearly everyone. You can’t drop indexOf straight into an if like this:

let str = "Bonfire beacon";

if (str.indexOf("Bonfire")) {
    alert("We found it"); // doesn't run!
}

The alert stays silent because str.indexOf("Bonfire") returns 0 — the match is at the start — and if treats 0 as falsy. A found-at-the-start result reads as “not found,” which is backwards.

The correct test compares against -1 explicitly:

let str = "Bonfire beacon";

if (str.indexOf("Bonfire") != -1) {
    alert("We found it"); // runs now!
}

includes, startsWith, endsWith

When you only care whether the substring is present, not where, reach for the newer str.includes(substr, pos). It returns a plain true or false, which sidesteps the 0-versus--1 trap entirely:

alert( "Bonfire beacon".includes("Bonfire") ); // true

alert( "Coral".includes("kelp") ); // false

Its optional second argument is, again, the position to start from:

alert( "Bonfire".includes("on") ); // true
alert( "Bonfire".includes("on", 2) ); // false, no "on" from position 2 onward

And str.startsWith and str.endsWith do precisely what their names promise:

alert( "Bonfire".startsWith("Bon") ); // true, "Bonfire" starts with "Bon"
alert( "Bonfire".endsWith("ire") ); // true, "Bonfire" ends with "ire"

Here all three ideas come together: type a needle and see every match highlighted, the first position from indexOf, and the plain true/false from includes. Notice the search is case-sensitive:

interactiveindexOf and includes over a haystack

Getting a substring

JavaScript hands you three methods for pulling a piece out of a string: slice, substring, and substr. They overlap heavily, and the differences are exactly the kind of thing that causes bugs, so it’s worth seeing them side by side.

Everything below works against this word and its positions:

0
a
-9
1
f
-8
2
t
-7
3
e
-6
4
r
-5
5
n
-4
6
o
-3
7
o
-2
8
n
-1
shaded band = slice(-4, -1)‘noo’  (positions 5, 6, 7)
afternoon indexed both ways. Positive positions count from 0 on the left; negative positions count from -1 on the right.
str.slice(start [, end])

Returns the piece from start up to — but not including — end.

let str = "afternoon";
alert( str.slice(0, 5) ); // 'after', characters 0 through 4
alert( str.slice(0, 1) ); // 'a', from 0 up to 1, so just the character at 0

Leave out end and slice runs to the end of the string:

let str = "afternoon";
alert( str.slice(2) ); // 'ternoon', from position 2 onward

start and end can be negative, in which case they count from the right end of the string:

let str = "afternoon";

// start 4 from the right, end 1 from the right
alert( str.slice(-4, -1) ); // 'noo'
str.substring(start [, end])

Returns the piece between start and end (not including end).

This is nearly identical to slice, with one difference: substring lets start be greater than end. When that happens it quietly swaps the two.

let str = "afternoon";

// these two are the same for substring
alert( str.substring(2, 6) ); // "tern"
alert( str.substring(6, 2) ); // "tern"

// ...but not for slice:
alert( str.slice(2, 6) ); // "tern" (same)
alert( str.slice(6, 2) ); // "" (empty string)

Negative arguments aren’t supported the way they are in slicesubstring treats any negative value as 0.

str.substr(start [, length])

Returns the piece starting at start, running for length characters.

This is the odd one out: the second argument is a count, not an end position.

let str = "afternoon";
alert( str.substr(2, 4) ); // 'tern', 4 characters starting at position 2

start may be negative, to count from the right:

let str = "afternoon";
alert( str.substr(-4, 2) ); // 'no', 2 characters starting 4 from the end

substr lives in Annex B of the language spec, the section for legacy browser features. In theory a non-browser engine could skip it. In practice it works everywhere.

The swap behavior is the sharpest edge between slice and substring, so it’s worth pinning down:

substring(6, 2)
→ swaps to substring(2, 6) → “tern”
slice(6, 2)
→ start is past end → “” (empty)
Give substring a start greater than end and it swaps them, returning the range anyway. slice never swaps — a backwards range gives you nothing.

Here’s the whole comparison in one place:

method selects… negatives
slice(start, end) from start to end (not including end) allows negatives
substring(start, end) between start and end (not including end) negative values mean 0
substr(start, length) from start, take length characters allows negative start

Move the two sliders and compare all three at once. The same numbers feed slice, substring, and substr — watch where they agree and where they diverge (try a first slider larger than the second, or a negative first value):

interactiveslice vs substring vs substr, side by side

Comparing strings

From the Comparisons chapter you know strings compare character by character, in what looks like alphabetical order. There are a couple of results that don’t match everyday expectations.

  1. A lowercase letter always ranks above the same-position uppercase letter:

    alert( 'a' > 'Z' ); // true
  2. Letters with diacritics land “out of order”:

    alert( 'Übungen' > 'Zenit' ); // true

    Sort a list of words and this bites you: most people expect Zenit to follow Übungen, not precede it.

The explanation is the UTF-16 encoding. Each character carries a numeric code, and strings are compared by those codes.

Two methods bridge characters and their codes:

str.codePointAt(pos)

Returns the numeric code of the character at position pos, as a decimal:

// letters of different case carry different codes
alert( "Q".codePointAt(0) ); // 81
alert( "q".codePointAt(0) ); // 113
alert( "q".codePointAt(0).toString(16) ); // 71 (hexadecimal, if you want it)

Full reference: str.codePointAt(pos).

String.fromCodePoint(code)

Builds a character from its numeric code:

alert( String.fromCodePoint(81) ); // Q
alert( String.fromCodePoint(0x51) ); // Q (a hex code works too)

Full reference: String.fromCodePoint(code).

Line up the codes from 65 to 220 — the Latin alphabet plus a bit extra — by turning each one into a character:

let str = '';

for (let i = 65; i <= 220; i++) {
  str += String.fromCodePoint(i);
}
alert( str );
// Output:
// ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
// ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜ

Notice the layout: capitals come first, then a few punctuation characters, then the lowercase letters, with Ö sitting far down near the end.

A … Z
codes 65–90
uppercase
<
a … z
codes 97–122
lowercase
<
À … Ö
codes 192–214
accented (a sample)
‘a’ (97) > ‘Z’ (90), so ‘a’ > ‘Z’ is true
Strings compare by numeric code. Uppercase letters occupy a lower range than lowercase, and many accented letters sit above both. That ordering, not the alphabet, decides greater-than.

Now a > Z makes sense. Comparison walks the numeric codes: the bigger code is the “bigger” character. The code for a is 97, the code for Z is 90, and 97 > 90.

  • Every lowercase letter outranks every uppercase letter, because its code is higher.
  • Letters like Ö sit apart from the core alphabet, with a code above everything from a to z.

Correct comparisons

Ordering text “properly” is harder than it looks, because every language sorts its alphabet differently. The browser has to know the language to get it right.

Modern browsers implement the internationalization standard ECMA-402, which exists to handle exactly this.

Its string method is str.localeCompare(str2). It reports how str ranks against str2 under the active language’s rules:

  • a negative number if str should come before str2,
  • a positive number if str should come after str2,
  • 0 if they’re considered equal.
alert( 'Übungen'.localeCompare('Zenit') ); // -1

So under locale rules, Übungen sorts before Zenit — the result people actually want. localeCompare also takes two more optional arguments, documented on MDN: one picks the language (it defaults to the environment, and letter order depends on it), and the other tunes rules like case sensitivity or whether "a" and "á" count as the same letter.

The difference is easy to feel with a real list. Edit the words if you like, then sort the same array two ways — by raw character code (<) and by localeCompare. Watch the accented and lowercase entries jump around:

interactiveRaw code order vs localeCompare

Summary

  • There are three quote styles. Backticks alone let a string span multiple lines and embed expressions with ${…}.
  • Special characters, such as the line break \n, are written with a leading backslash.
  • To read one character, use [] or at.
  • To pull out a substring, use slice (or substring).
  • To change case, use toLowerCase and toUpperCase.
  • To search, use indexOf, or includes / startsWith / endsWith for simple presence checks.
  • To compare the way a reader expects, use localeCompare; otherwise strings compare by character code.

A few more methods worth keeping in your back pocket:

  • str.trim() removes whitespace from both ends of the string.
  • str.repeat(n) glues n copies of the string together.
  • …and the rest are catalogued in the String reference.

Strings can also search and replace using regular expressions. That’s a large topic with its own section: regular expressions.

Keep in mind that strings rest on Unicode, which is where the comparison quirks come from. The Unicode, String internals chapter goes deeper into how that encoding works and how to handle the characters that span two code units.