Strings and Template Literals
Working with text: quotes and escapes, template literals with ${} placeholders, common string methods, Unicode pitfalls, and why strings never change in place.
Creating strings
A string is a
sequence of characters. In JavaScript you write one between single
quotes ('…'), double quotes ("…") or
backticks (`…`). Single and double quotes are
completely equivalent; teams simply pick one style and let a
formatter enforce it. Backticks create a
template literal, which can do
more.
Every string has a length property (a property, not a
function, so no parentheses). Square brackets and
charAt() read one character by its index, starting at
0. at() also accepts negative numbers, which count from
the end.
const site = 'Kestrel Energy';
const note = "Meter can't be read";
const reply = 'Customer said "the display is blank"';
console.log(site, '|', note, '|', reply);
console.log(site.length);
console.log(site[0], site.charAt(8), site.at(-1));
console.log(site[99], site.charAt(99) === '');
Kestrel Energy | Meter can't be read | Customer said "the display is blank" 14 K E y undefined true
An index past the end does not throw an error: square brackets give
undefined and charAt() an empty string.
That is quieter than Python, which raises an
IndexError, so a typo in an index can go unnoticed.
Escape sequences
Inside quotes, a backslash starts an escape sequence: a short code for a character that is hard to type or would end the string. JavaScript uses the same common codes as Python and C:
\nnew line,\ttab-
\\one backslash;\',\"and\`a quote that does not end the string -
\u20ACa character by its four-digit hexadecimal code (here €), and\u{1F50C}any code point, with braces, including ones above FFFF
console.log('Line one\nLine two');
console.log('It\'s a backslash: \\');
console.log('Tariff: \u20AC0.27 per kWh');
console.log('Charger: \u{1F50C}');
console.log('a\nb'.length); // \n is one character
Line one Line two It's a backslash: \ Tariff: €0.27 per kWh Charger: 🔌 3
String.raw (see
tagged templates) keeps backslashes
as typed, much like a Python raw string:
String.raw`C:\temp\new` is the text
C:\temp\new with both backslashes intact.
Template literals
A template literal is a
string written between backticks. It differs from a normal string
literal in two ways: it may contain real line breaks, and it may
contain placeholders of the form ${expression}. Since
ES2015 (the 2015 edition of the language standard) it has been the
usual way to build text from values.
String interpolation
Filling placeholders with values is called
string interpolation.
Between ${ and } you can put any
expression: a variable, arithmetic, a function call, even a
conditional (ternary) expression. Its value is converted to text and
inserted. This replaces long chains of +.
const ticket = 'INC-31877';
const customers = 412;
const minutes = 95;
// Old style: concatenation with +
console.log('Outage ' + ticket + ' affects ' + customers + ' customers.');
// Template literal: the same text, easier to read
console.log(`Outage ${ticket} affects ${customers} customers.`);
console.log(`Duration: ${Math.floor(minutes / 60)} h ${minutes % 60} min`);
console.log(`Status: ${customers > 100 ? 'major' : 'minor'} incident`);
Outage INC-31877 affects 412 customers. Outage INC-31877 affects 412 customers. Duration: 1 h 35 min Status: major incident
Line breaks inside the backticks become part of the string, including any indentation, so a multi-line template is usually written flush against the left margin:
const meter = 'KE-0042';
const kwh = 318.4;
const message = `Dear customer,
Your meter ${meter} recorded ${kwh} kWh this month.
Kestrel Energy`;
console.log(message);
console.log(message.split('\n').length, 'lines');
Dear customer, Your meter KE-0042 recorded 318.4 kWh this month. Kestrel Energy 3 lines
Tagged templates
A tagged template is a
template literal with a function name in front of it, such as
html`…`. Instead of building a string straight away,
JavaScript calls that function (the tag) with two things:
an array of the fixed text parts, and the placeholder values as
separate arguments. The tag decides what to return. Libraries use
this for safe SQL queries, HTML templates and styling.
The example below is a tag that escapes the inserted values before
they go into HTML, so a customer name containing
< cannot become markup. That is the idea behind
output encoding, a key defense against
cross-site scripting. In real projects, use a well-tested library or a framework's
built-in escaping rather than your own.
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
}
// strings: the fixed parts; values: what the placeholders produced
function html(strings, ...values) {
console.log(strings);
console.log(values);
return strings.reduce(
(out, text, i) => out + escapeHtml(values[i - 1]) + text
);
}
const name = 'Ravi <b>Kumar</b>';
const balance = 42.5;
console.log(html`<p>Hello ${name}, you owe €${balance}</p>`);
[ '<p>Hello ', ', you owe €', '</p>' ] [ 'Ravi <b>Kumar</b>', 42.5 ] <p>Hello Ravi <b>Kumar</b>, you owe €42.5</p>
The built-in tag String.raw returns the text with
escape sequences left as typed, which is handy for Windows paths and
regular expressions:
console.log(String.raw`C:\Users\amara\new_readings.csv`);
console.log(String.raw`\d+ kWh`.length); // backslash, d, +, space, k, W, h
C:\Users\amara\new_readings.csv 7
Strings never change in place
Strings are immutable: once created, their characters cannot be changed. Assigning to an index does not work. In ordinary script code it fails silently, which is why it is a classic source of bugs:
let code = 'ke-0042';
code[0] = 'K'; // ignored, no error
console.log(code);
code = code.toUpperCase(); // build a new string and keep it
console.log(code);
ke-0042 KE-0042
In strict mode (the default inside modules and classes; see Running JavaScript), the same assignment throws an error, which is much easier to notice:
'use strict';
const code = 'ke-0042';
code[0] = 'K';
TypeError: Cannot assign to read only property '0' of string 'ke-0042'
The practical rule is the same as in Python: every string method
returns a new string. Calling name.trim()
without storing the result does nothing useful.
Everyday string methods
Methods are called with a dot: text.trim(). The table
lists the ones you will use most; the examples below show them on
real-looking data.
| Task | Methods |
|---|---|
| Take part of a string |
slice(start, end), substring(),
at(), charAt()
|
| Search |
includes(), startsWith(),
endsWith(), indexOf(),
lastIndexOf()
|
| Clean up |
trim(), trimStart(),
trimEnd(), toLowerCase(),
toUpperCase()
|
| Split and join |
split(separator), and the array method
join(separator)
|
| Replace | replace(), replaceAll() |
| Pad and repeat |
padStart(), padEnd(),
repeat()
|
Taking part of a string
slice(start, end) returns the characters from
start up to, but not including, end, like
a Python slice. Leave out end to go to the end of the
string; negative values count from the end.
substring() is an older method that looks the same but
treats negative values as 0 and swaps its arguments if
start is larger; prefer slice().
const reading = '2026-10-01T08:30 KE-0042 318.4';
console.log(reading.slice(0, 10)); // the date
console.log(reading.slice(11, 16)); // the time
console.log(reading.slice(-5)); // the last five characters
console.log(reading.slice(17, -6)); // the meter id
console.log(reading.substring(-3, 4), reading.slice(-3, 4) === '');
2026-10-01 08:30 318.4 KE-0042 2026 true
Searching
includes(), startsWith() and
endsWith() answer yes-or-no questions.
indexOf() returns the position of the first match, or
-1 if there is none, so test it with
!== -1, never as a truth value: a match at position 0
is falsy. All searches are case-sensitive.
const subject = 'Outage INC-31877: no power in Delft';
console.log(subject.includes('INC-'), subject.includes('inc-'));
console.log(subject.startsWith('Outage'), subject.endsWith('.'));
console.log(subject.indexOf('INC-'), subject.indexOf('Leiden'));
console.log(subject.toLowerCase().includes('delft'));
true false true false 7 -1 true
Cleaning, splitting and joining
Text from forms, files and other systems often has stray spaces and
mixed case. trim() removes whitespace at both ends.
split() cuts a string into an array at every separator;
the array method join() glues the parts back with a
separator of your choice. Unlike Python,
split() without an argument does not split on
whitespace: it returns the whole string in a one-element array.
const email = ' Amara.Okafor@Example.com \n';
const clean = email.trim().toLowerCase();
console.log(clean);
console.log(clean.split('@'));
const line = 'KE-0042;318.4;kWh;2026-10-01';
const [meter, value, unit] = line.split(';');
console.log(meter, Number(value) * 2, unit);
console.log('2026-10-01'.split('-').join('/'));
console.log('one two'.split(' '));
console.log('one two'.split());
amara.okafor@example.com [ 'amara.okafor', 'example.com' ] KE-0042 636.8 kWh 2026/10/01 [ 'one', '', 'two' ] [ 'one two' ]
The line const [meter, value, unit] = … unpacks the
array into three variables; this is destructuring, explained on the
Arrays page.
Replacing
replace(a, b) with a string replaces only the
first match, which surprises people coming from Python.
replaceAll() (ES2021) replaces every match. Both also
accept a
regular expression; with replace(), the g (global) flag
makes it replace all matches.
const csv = '318,4;290,1;305,7';
console.log(csv.replace(',', '.')); // first match only
console.log(csv.replaceAll(',', '.')); // every match
console.log(csv.replace(/,/g, '.')); // regular expression, g flag
console.log('Call us today'.replace(/\s+/g, ' '));
318.4;290,1;305,7 318.4;290.1;305.7 318.4;290.1;305.7 Call us today
Padding and repeating
padStart(width, fill) and
padEnd(width, fill) add characters until the string
reaches a given length. They are the way to make fixed-width
columns, ticket numbers with leading zeros and simple text tables.
repeat(n) repeats a string.
const outages = [
['Delft', 412],
['Gouda', 37],
['Leiden', 1208],
];
console.log('Town'.padEnd(10) + 'Customers'.padStart(10));
console.log('-'.repeat(20));
for (const [town, count] of outages) {
console.log(town.padEnd(10) + String(count).padStart(10));
}
console.log(`INC-${String(877).padStart(5, '0')}`);
Town Customers -------------------- Delft 412 Gouda 37 Leiden 1208 INC-00877
Strings and numbers
The + operator does two jobs: addition and
concatenation. If either side is a string, JavaScript converts the
other side to a string and joins them. The other arithmetic
operators (-, *, /) only do
math, so they convert strings to numbers instead. This automatic
conversion is called type coercion; the full rules are on
Operators, Comparison and Type Coercion.
console.log('5' + 3); // string wins: concatenation
console.log('5' - 3); // minus only does math
console.log('5' * '3');
console.log(1 + 2 + ' kWh'); // left to right: 3, then '3 kWh'
console.log('kWh ' + 1 + 2); // 'kWh 1', then 'kWh 12'
53 2 15 3 kWh kWh 12
Values read from a form field, a file or the command line are always
strings. Convert them explicitly before calculating:
Number(text) converts the whole string (and gives
NaN if it is not a number), String(value)
or a template literal converts the other way. The details of parsing
and formatting numbers are on
Numbers, Math and Dates.
const fromForm = '318.4'; // what an input field gives you
console.log(fromForm + 10); // wrong: concatenation
console.log(Number(fromForm) + 10); // right: convert first
console.log(String(42) + '%', `${42}%`);
console.log(Number('12 kWh'));
318.410 328.4 42% 42% NaN
Unicode, UTF-16 and length
JavaScript strings are sequences of 16-bit
UTF-16
code units, a design from the mid-1990s, when 16 bits seemed enough
for every character. length, indexing and
slice() all count these code units. For letters such as
é, ü or € that is one unit per character, so nothing looks wrong.
Many emoji and some rarer scripts sit above code point FFFF,
however, and need two code units.
Surrogate pairs
A surrogate pair is the two
UTF-16 code units that together encode one code point above FFFF: a
high surrogate (D800–DBFF) followed by a low surrogate (DC00–DFFF).
Neither half means anything on its own. Because
length and indexes count code units, a single emoji
reports a length of 2, and slicing between the two halves breaks the
character.
"€🔌".length is 3.
The plug emoji is stored as a surrogate pair (highlighted).
Anything that iterates a string, such as the spread syntax
[...text], Array.from(text) or a
for...of loop, walks by code point instead and keeps pairs together.
codePointAt() reads a full code point, and
String.fromCodePoint() builds one. In the example,
slice(0, 4) keeps only the high surrogate: a lone half
that Node prints as the replacement character �, and that
isWellFormed() (ES2024) reports as broken.
const label = 'EV 🔌';
console.log(label.length); // 5 code units
console.log([...label].length); // 4 code points
console.log(label.slice(0, 4)); // cuts the pair in half
console.log(label.slice(0, 4).isWellFormed());
console.log([...label].slice(0, 4).join(''));
console.log(label.codePointAt(3).toString(16));
console.log(String.fromCodePoint(0x1f50c) === '🔌');
5 4 EV � false EV 🔌 1f50c true
Even code points are not always what a reader calls a character. Some emoji (flags, skin tones, families) combine several code points, and an accented letter can be stored as one code point (é) or as a letter plus a combining accent. Two practical tools:
-
normalize("NFC")turns both spellings of é into the same code points, so comparisons and searches agree. Normalize text from different sources before comparing it. -
Intl.Segmentersplits text into user-perceived characters (grapheme clusters). Use it when you must count or cut visible characters, for example for a length limit on a display.
const composed = 'caf\u00e9'; // é as one code point
const decomposed = 'cafe\u0301'; // e + combining acute accent
console.log(composed, decomposed);
console.log(composed === decomposed, composed.length, decomposed.length);
console.log(composed === decomposed.normalize('NFC'));
const flag = '🇳🇱';
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
console.log(flag.length, [...flag].length, [...segmenter.segment(flag)].length);
café café false 4 5 true 4 2 1
Comparing and sorting strings
=== compares two strings exactly, character by
character, including case and spaces. < and
> compare by UTF-16 code unit, so all uppercase
ASCII letters come before all lowercase ones, and digits in text are
compared as characters. For text shown to people, use
localeCompare(), which follows the rules of a language,
or an Intl.Collator when you sort many strings.
console.log('Delft' === 'delft', 'Delft'.toLowerCase() === 'delft');
console.log('Zwolle' < 'amsterdam'); // 'Z' (90) < 'a' (97)
console.log('10' < '9'); // '1' < '9'
const towns = ['zwolle', 'Échirolles', 'Amsterdam', 'delft'];
console.log([...towns].sort());
console.log([...towns].sort((a, b) => a.localeCompare(b)));
const byNumber = new Intl.Collator('en', { numeric: true });
console.log(['meter-10', 'meter-9', 'meter-100'].sort(byNumber.compare));
false true true true [ 'Amsterdam', 'delft', 'zwolle', 'Échirolles' ] [ 'Amsterdam', 'delft', 'Échirolles', 'zwolle' ] [ 'meter-9', 'meter-10', 'meter-100' ]
The default sort() compares code units, which puts
"Échirolles" after every ASCII name.
localeCompare() sorts it where a reader expects. The
numeric option sorts meter-9 before
meter-10. Sorting arrays in general, including why
[10, 9, 1].sort() gives a surprising order, is on the
Arrays page.