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:

  • \n new line, \t tab
  • \\ one backslash; \', \" and \` a quote that does not end the string
  • \u20AC a 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('&', '&amp;')
    .replaceAll('<', '&lt;')
    .replaceAll('>', '&gt;')
    .replaceAll('"', '&quot;')
    .replaceAll("'", '&#39;');
}

// 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 &lt;b&gt;Kumar&lt;/b&gt;, 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.

The string "€🔌" in UTF-16: the euro sign is one code unit, 20AC; the plug emoji, code point 1F50C, is a surrogate pair of two code units, D83D and DD0C. The length is 3. Characters Code points Code units € plug emoji U+20AC U+1F50C 20AC D83D DD0C index 0 index 1 index 2
Two characters, three code units: "€🔌".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.Segmenter splits 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.