The complete guide to

Working With Strings in Modern JavaScript

This guide is intended to cover everything you need to know about creating, manipulating and comparing strings in JavaScript.

extra tips

and

common issues

sections along the way will hopefully teach you something new, and help you to avoid common mistakes.

If I've missed anything, please let me know and I'll add it!

Remember to bookmark this page to refer to later!

Contents

Creating strings

There are fundamentally two categories of strings in JavaScript; string primitives and String objects.

Primitives

String primitives are created like this:

let example1 = "BaseClass"
// or 
let example2 = String("BaseClass")

In almost all cases, you should use one of these methods to create a new string.

You can use either single quotes (' ') or double-quotes (" ") when defining a string literal.

Objects

You can create a String object using the new keyword.

The only real advantage that an object has over a string primitive is that you can attach additional properties to it:

let website = new String("BaseClass")
website.rating = "great!"

There are very few cases where this is useful though. In almost all cases, you should create a string primitive.

All of the string methods you are familiar with are part of the String object, not the primitive.

When you call a method on a string primitive, JavaScript 'auto-boxes', or wraps, the primitive in a String object, and calls the method on that object instead.

Template Literals

Basic Template Literals

Template Literals allow you to combine variables and text into a new string using a more readable syntax.

Instead of double or single-quotes, enclose the string in back-ticks and reference variables using the syntax ${variableName}:

Before

let opinion = "Love"
let tweet = "I " + opinion + " JavaScript"

// "I Love JavaScript"

After

let opinion = "Love"
let tweet = `I ${opinion} JavaScript`

// "I Love JavaScript"

You can also include expressions in a Template Literal:

let age = 37
let result = `You ${age > 30 ? "do" : "don't"} remember VHS tapes!`

"You do remember VHS tapes!"

Brower support for Template Literals is now very good:

Template Literals
  • Chrome

    41+

  • Edge

    13+

  • Firefox

    34+

  • Safari

    9.1+

  • Opera

    29+

You can also nest templates, as shown in this example from MDN:

const classes = `header ${ isLargeScreen() ? '' 
        :  `icon-${item.isCollapsed ? 'expander' : 'collapser'}` 
    }`;

Tagged Templates

Tagged Templates allow you to create a function that parses a Template Literal.

This can be really powerful, and is most clearly demonstrated by an example:

Imagine we have a function called censor() that removes any offensive words in a string entered by a user.

When we want to censor a string, we could manually call censor() on every user entered value:

let response = `Hi ${censor(name)}, you entered ${censor(jobTitle)}.`

Or, we could use Tagged Templates.

This allows us to write a function that accepts the string values from the Template Literal, and all of the expressions used in the template:

let censorStrings = (strings, name, jobTitle) => {
    // strings: ["Hi ", ", you entered ", "."]
    // name: "Dave"
    // jobTitle: "Developer"
}

let name = "Dave"
let jobTitle = "Developer"

let response = censorStrings`Hi ${name}, you entered ${jobTitle}.`

Note that in the last line we 'tag' the string with our function by adding it before the Template Literal, rather than explicitly calling the censorStrings() function.

This means we can now manipulate the template literal and the values inside it.

The first argument to the tagging function is always an array of strings. The remaining arguments represent each variable/expression uses in the Template Literal.

That means that you won't necessarily know how many arguments to expect in your 'tagging' function.

In those cases, it's useful to put each of the remaining arguments in to an array (using the 'rest parameters' syntax), so that you can iterate through them:

let censorStrings = (strings, ...inputs) => {
    // strings: ["Hi ", ", you entered ", "."]
    // inputs: ["Dave", "Developer"]
}

Now we have access to the Template Literal and the individual arguments, we can sensor every variable used in the string:

let censorStrings = (strings, ...inputs) => {
    // Censor each variable used in the template literal
    let censored = inputs.map(i => censor(i))
}

let name = "Dave"
let jobTitle = "Developer"

let response = censorStrings`Hi ${name}, you entered ${jobTitle}.`

Finally, our tagging function has to return the finished string.

To do this, we simply interleave the original string array and the array of (modified) inputs in to a new array.

// (strings)            (inputs)
// "Hi "            ->
//                      <- "Dave"
// ", you entered " ->
//                      <- "Developer"
// "."              ->

// "Hi Dave, you entered Developer."

Here we do that using .reduce():

return strings.reduce((result, currentString, i) => (
    `${result}${currentString}${censored[i] || ''}`
), '')

Our tagging function is now complete, and can be used anywhere we need to censor user inputs:

let censorStrings = (strings, ...inputs) => {
    // Censor each variable used in the template literal
    let censored = inputs.map(i => censor(i))

    // Interleave the string and (censored) inputs 
    // to create the finished string
    return strings.reduce((result, currentString, i) => (
        `${result}${currentString}${censored[i] || ''}`
    ), '')
}

let name = "Dave"
let jobTitle = "Developer"

let response = censorStrings`Hi ${name}, you entered ${jobTitle}.`

// "Hi Dave, you entered Developer."

The tagging function does not need to return a string.

For example, there are libraries for React that take the Template Literal and return a React component.

Raw Strings

String.raw is a pre-defined tag function.

It allows you to access the string without interpreting any backslash escape sequences.

For example, when using a string containing \n with String.raw, instead of getting a new line in the string you would get the actual \ and n characters:

// NOT using String.raw
`Hello\nWorld`
// Hello
// World

// WITH String.raw
String.raw`Hello\nWorld`
// Hello\nWorld

This makes it useful for (among other things) writing strings in which you'd usually have to escape lots of backslash characters, such as file paths:

// Without string.raw, we have to escape 
// the \ folder separators
var path = `C:\\Program Files\\file.json`

// With string.raw, no need for escape sequences
var path = String.raw`C:\Program Files\file.json`

When using string.raw, the \ character does escape the final back-tick.

That means that you cannot end a raw string with a \ like this:

// The final back-tick (`) would be escaped, meaning this 
// string is not terminated correctly
String.raw`c:\Program Files\`

Combining strings

Concatenating strings

You can combine (or 'concatenate') multiple strings to create a new one using the + symbol:

let name = "Base" + "Class"

// Output: "BaseClass"

This can also be used to split string creation over multiple lines for readability:

let name = "This really is " +
    "a very " +
    "long string"

// "This really is a very long string"

You can also concatenate strings with variables (non-string variables will be converted to strings):

let precinct = 99
let name = "Brooklyn " + precinct

// Output: "Brooklyn 99"

To create a new string by adding to the end of an existing one, use +=:

let name = "Three "
name += "Blind "
name += "Mice"

// "Three Blind Mice"

You can also concatenate strings and variables using the string.concat() method, but that is not recommended for performance reasons.

Instead, use the + or += operators above

Repeating a string

The repeat() method returns a new string, which contains the original string repeated a number of times:

let warning = "Londons Burning. "
let result = warning.repeat(2)

// "London's Burning. London's Burning. "        

You can use this in the following browsers:

string.repeat()
  • Chrome

    41+

  • Edge

    12+

  • Firefox

    24+

  • Safari

    9+

  • Opera

    28+

Joining strings

You can combine an array of strings into one using the .join() method on the array.

The default is to separate the items with a comma:

let heros = ["Superman", "Wonder Woman", "Hulk"]
heros.join()

// "Superman,Wonder Woman,Hulk"

You can also specify the string used to separate the elements:

let heroes = ["Superman", "Wonder Woman", "Hulk"]
heroes.join(" or ")

// "Superman or Wonder Woman or Hulk"

Passing an empty string to string.join will join elements with nothing between them:

let heroes = ["Superman", "Wonder Woman", "Hulk"]
heroes.join("")

// "SupermanWonder WomanHulk"

When toString() is used on an array, it also returns a comma-separated list of the strings.

let heroes = ["Superman", "Wonder Woman", "Hulk"]
heroes.toString()

// Output: Superman,Wonder Woman,Hulk"

Splitting a string

You can split a string in to an array using the split() method.

Common use cases for this are:

Turning a sentence into an array of words, by splitting it on spaces:

"Welcome to Paradise".split(" ")
// [ "Welcome", "to", "Paradise" ]

.. or splitting a multi-line string into individual lines:

let song = `Hello,
Is it me you're looking for?`

song.split("\n")

// [ "Hello,", "Is it me you're looking for?" ]

You can also limit the number of items you would like back from split(), by passing an optional second parameter:

// Get only the first two words
"The quick brown fox".split(" ", 2)

// Output: [ "The", "quick" ]

If you need to turn a string in to an array of characters, the split() method does not work well for Unicode characters that are represented by 'surrogate pairs':

"👋!".split("") // ["�", "�", "!"]

In modern browsers, you can use the spread operator instead:

[..."👋!"] // ["👋", "!"]

Comparing strings

Equality

When you know you are comparing two string primitives, you can use either the == or === operators:

"abba" === "abba" // true
"abba" == "abba" // true

If you are comparing a string primitive to something that is not a string, == and === behave differently.

When using the == operator, the non-string will be coerced to a string. That means that JavaScript will try and make it a string before comparing the values.

9 == "9"
// becomes 
"9" == "9" //true

For a strict comparison, where non-strings are not coerced to strings, use ===:

9 === "9" // false

The same is true of the not-equal operators, != and !==:

9 != "9" // false
9 !== "9" // true

If you're not sure what to use, prefer strict equality using ===.

When using String objects, two objects with the same value are not considered equal:

new String("js") == new String("js") // false
new String("js") === new String("js") // false

Case sensitivity

When a case-insensitive comparison is required, it is common to convert both strings to upper or lowercase and compare the result.

"Hello".toUpperCase() === "HeLLo".toUpperCase() // true

Sometimes you need more control over the comparison, though. See the next section for that..

Handling diacritics

Diacritics are modifications to a letter, such as é or ž.

You may wish to specify how these are handled when comparing two strings.

For example, in some languages it is common practice to exclude accents from letters when writing in uppercase.

If you need a case-insensitive comparison, simply converting two strings to the same case first using toUpperCase() or toLowerCase() would not account for this addition/removal of accents, and might not give you the result you are expecting.

When you need more fine-grained control of the comparison, use localeCompare instead:

let a = 'Résumé';
let b = 'RESUME';
   
// Returns 'false' - the strings do not match
a.toLowerCase() === b.toLowerCase()
   
// Returns '0' - the strings match
a.localeCompare(b, undefined, { sensitivity: 'base' })

The localeCompare method allows you to specify the 'sensitivity' of the comparison.

Here we used a 'sensitivity' of base to compare the strings using their 'base' characters (meaning it will ignore case and accents).

This is supported in the following browsers:

localeCompare()
  • Chrome

    24+

  • Edge

    12+

  • Firefox

    29+

  • Safari

    10+

  • Opera

    15+

Greater/less than

When comparing strings using the < and > operators, JavaScript will compare each character in 'lexicographical order'.

That means that they are compared letter by letter, in the order they would appear in a dictionary:

"aardvark" < "animal" // true
"gamma" > "zulu" // false

When comparing strings using < or >, lowercase letters are considered larger than uppercase.

"aardvark" > "Animal" // true

This is because JavaScript is actually using each character's value in Unicode, where lowercase letters are after uppercase letters.

True or false strings

Empty strings in JavaScript are considered false when compared with the == operator (but not when using ===)

("" == false) // true
("" === false) // false

Strings with a value are 'true', so you can do things like this:

if (someString) {
    // string has a value
} else {
    // string is empty or undefined
}

Sorting strings

Simple Array.sort()

The simplest way to sort an array of strings is to use the Array.sort() method:

["zebra", "aligator", "mouse"].sort()
// [ "aligator", "mouse", "zebra" ]

When sorting an array of strings, they are compared using each character's 'UTF-16 code unit' value

In Unicode, uppercase letters are before lowercase letters.

That means that strings that start with a capital letter will always end up before strings starting with lowercase letters:

["zebra", "aligator", "Mouse"].sort()
// [ "Mouse", "aligator", "zebra" ]

You can work around this by converting all of the strings to the same case first, or by using localeCompare (see below), which is usually more efficient

localeCompare

Using localeCompare as the sorting function allows you to compare strings in a case-insensitive way:

let animals = ["zebra", "aligator", "Mouse"]
animals.sort((a, b) => a.localeCompare(b));

/// [ "aligator", "Mouse", "zebra" ]

You can also use localeCompare to ignore diacritics (like accents) while sorting strings. See the Handling Diacritics section for more information.

Multi-line strings

You can add new lines in a string using \n:

let result = "The winner is:\nBaseClass!"

// The winner is:
// BaseClass!

In a Template Literal, new lines are respected within the backticks:

let result = `The winner is:
BaseClass!`

// The winner is:
// BaseClass!

In Template Literals, you can escape line-breaks by adding a \ at the end of the line.

let result = `All on \
    one line`

// "All on one line"

Padding strings

You can add whitespace to the start or end of a string until it reaches a specified length using padStart() or padEnd():

// Pad the string "hello" with whitespace to make 
// it 8 characters long:

"hello".padStart(8) // "   hello"
"hello".padEnd(8) // "hello   "

Instead of whitespace, you can pad the target string with another string, by passing it as the second parameter.

That string will be repeated until the target length is reached (the string will be truncated if it does not fit evenly into the required padding):

// Pad "Train" to 13 characters using the string "Choo"
"Train".padStart(13, "Choo")

// Output: "ChooChooTrain"

This is supported by all modern browsers:

string.padStart()
string.padEnd()
  • Chrome

    57+

  • Edge

    15+

  • Firefox

    48+

  • Safari

    10+

  • Opera

    44+

Extracting part of a String

Substrings

These methods take the index of the first character you wish to extract from the string.

They return everything from that character to the end of the string:

"I am Groot!".slice(5) // "Groot!"
"I am Groot!".substring(5) // Groot!

The second (optional) argument is the character at which you'd like to stop.

This final character is not included in the output:

// Extract a new string from the 5th character, 
// up to (but not including!) the 10th character

"I am Groot!".slice(5, 10) // "Groot"
"I am Groot!".substring(5, 10) // "Groot"

So, which one should you use?

They are very similar, but with some subtle differences:

There is also a substr() method, that is similar to slice() and substring().

This is a legacy API. While it is unlikely to be used soon, you should use one of the two methods above instead where possible.

Single characters

The charAt() method returns a specific character from the string (remember, indexes are 0-based):

"Hello".charAt(1) // "e"

You can also treat a string as an array, and access it directly like this:

"Hello" [1] // e

Accessing a string as an array could lead to confusion when the string is stored in a variable.

Using charAt() is more explicit:

// What is 'someVariable'?
let result = someVariable[1]

// 'someVariable' is clearly a string
let result = someVariable.charAt(1)

Changing the case of a string

You can make a string all-caps like this:

"hello".toUpperCase() // "HELLO

Or all lowercase like this:

"Hello".toLowerCase() // "hello

These methods are commonly used to convert two strings to upper/lowercase in order to perform a case-insensitive comparison of them.

Depending on the strings you are comparing, you might want more control over the comparison. Consider using localeCompare instead.

See this section.

Removing whitespace

The following methods will remove all spaces, tabs, non-breaking spaces and line ending characters (e.g. \n) from relevant part the string:

"  Trim Me  ".trim() // "Trim Me"
"  Trim Me  ".trimStart() // "Trim Me  "
"  Trim Me  ".trimEnd() // "  Trim Me"

"With Newline\n".trimEnd() // "With NewLine"    

trimStart() and trimEnd() were introduced in ES10, and are now the 'preferred' methods to use according to that specification.

At the time of writing, however, these are not supported in the Edge browser.

For compatibility in all modern browsers, use trimLeft() and trimRight():

"  Trim Me  ".trimLeft() // "Trim Me  "
"  Trim Me  ".trimRight() // "  Trim Me"

Searching for text in a string

Find the position of a substring

You can search for a string within another string using indexOf().

This will return the position of first occurrence of the search term within the string, or -1 if the string is not found:

"Superman".indexOf("man") // '5'
"Superman".indexOf("foo") // '-1'

You can also use the regular expression method search() to do the same thing:

"Superman".search("man") // '5'

To search for the last occurrence of a search term, use lastIndexOf():

"Yabba Dabba Doo".indexOf("bb") // '2'
"Yabba Dabba Doo".lastIndexOf("bb") // '8'

All of these methods will return -1 if the search term is not found in the target string.

Starts/Ends with

You can use the indexOf() methods above to check whether a string begins with, or ends with, a search term.

However, ES6 added specific methods for this:

"Wonder Woman".startsWith("Wonder") // true
"Wonder Woman".endsWith("Woman") // true
string.startsWith()
string.endsWith()
  • Chrome

    41+

  • Edge

    12+

  • Firefox

    17+

  • Safari

    9+

  • Opera

    28+

Includes

If you don't care about the specific position of the substring, and only care whether it is in the target string at all, you can use includes():

"War Games".includes("Game") // true
string.includes()
  • Chrome

    41+

  • Edge

    12+

  • Firefox

    40+

  • Safari

    9+

  • Opera

    28+

Regular Expressions

This section is an overview.

I now have a full length course on Regular Expressions here.

To find the first occurrence of a regular expression match, use .search().

// Find the position first lowercase character
"Ironman 3".search(/[a-z]/) // '1'

To return an array containing all matches of a regular expression, use match(), with the /g (global) modifier:

// Return all uppercase letters
"This is England".match(/[A-Z]/g)

// Output: [ "T", "E" ]    

(using match() without the /g modifier will return only the first match, and some additional properties such as the index of the result in the original string, and any named capturing groups)

If you need more details about each match, including their index in the original string, you can use matchAll.

This method returns an iterator, so you can use a for...of loop over the results. You must use a regular expression with the /g/ modifier with matchAll():

// Find all uppercase letters in the input
let input = "This is England"
let matches = input.matchAll(/[A-Z]/g)

// Turn the result into an array, using 
// the spread operator
[...matches]

// Returns:
// [
//   ["T", index: 0, groups: undefined]
//   ["E", index: 8, groups: undefined]
// ]

Replacing characters in a string

You can use replace() to replace certain text in a string.

The first argument to replace() is the text to find and replace, the second is the text to replace it with.

Passing a string as the first argument replaces only the first instance of the term:

"no no no!".replace("no", "yes")
// "yes no no!"

If you want to replace all instances of the text, you can pass a regular expression with the 'greedy' modifier (/g) as the first argument:

"no no no!".replace(/no/g, "yes")
// "yes yes yes!"

For more information on Regular Expressions, check out the free course.

ES2021 added replaceAll(), to make replacing all instances of a term easier:

"no no no!".replaceAll("no", "yes")
// "yes yes yes!"

You can use this if you only need to support very recent browsers:

string.replaceAll()
  • Chrome

    85+

  • Edge

    85+

  • Firefox

    77+

  • Safari

    13.1+

  • Opera

    71+