Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. JavaScript
  3. JavaScript String Methods Practice
JavaScript17 exercises

JavaScript String Methods Practice

Practice JS string methods: slice vs substring (negative indices), replace vs replaceAll (regex /g gotcha), template literals, tagged templates, and Unicode pitfalls (emoji length).

Common ErrorsQuick ReferencePractice
Warm-up1 / 2

Can you write this from memory?

Check if string `item_text` matches the pattern `/abc/`.

On this page
  1. 1slice() vs substring(): the index behavior difference
  2. 2replace() vs replaceAll(): the first-match trap
  3. The regex /g requirement
  4. When to use which
  5. 3Template literals: more than string interpolation
  6. 1. Interpolation
  7. 2. Multi-line strings
  8. 3. Tagged templates
  9. 4Unicode: why string.length lies
  10. Why this happens
  11. Counting actual characters (graphemes)
  12. When this matters
  13. 5Locale-aware string operations
  14. Comparing strings with accents
  15. For repeated comparisons, use Intl.Collator
  16. normalize() for string equality
  17. 6Common recipes
  18. slugify (URL-safe string)
  19. camelCase to kebab-case
  20. Safe HTML escaping
  21. Truncate with ellipsis (grapheme-safe)
  22. 7Quick reference
  23. 8References
slice() vs substring(): the index behavior differencereplace() vs replaceAll(): the first-match trapTemplate literals: more than string interpolationUnicode: why string.length liesLocale-aware string operationsCommon recipesQuick referenceReferences

slice or substring? In JavaScript, slice supports negative indices; substring clamps negatives to 0. replace or replaceAll? replace only hits the first match unless you use a regex with /g. Why is '😄'.length === 2? JavaScript strings are UTF-16, not Unicode graphemes.

JavaScript string methods look similar but behave differently. We drill the distinctions that cause real bugs: index handling, replacement patterns, and Unicode surprises.

Quick reference:

  • slice(-2) → last 2 characters
  • substring(-2) → same as substring(0) (negatives clamped)
  • replace("x", "y") → first match only
  • replaceAll("x", "y") → all matches
  • replaceAll(/x/, "y") → TypeError (needs /g flag)
  • '👍'.length → 2 (UTF-16 code units, not characters)
Related JavaScript Topics
JavaScript CollectionsJavaScript FunctionsJavaScript Modules & Scope

Use slice() over substring(). It supports negative indices (count from end), doesn't swap arguments, and matches array behavior.

Both extract substrings, but they handle edge cases differently:

const str = "JavaScript";

// Basic usage (identical)
str.slice(0, 4)      // "Java"
str.substring(0, 4)  // "Java"

// Negative indices (different!)
str.slice(-6)        // "Script" (counts from end)
str.substring(-6)    // "JavaScript" (negatives → 0)

// Reversed arguments
str.slice(4, 0)      // "" (empty, no swapping)
str.substring(4, 0)  // "Java" (swaps to 0, 4)

Rule of thumb: Use slice(). It's more predictable and matches array behavior. For a printable reference of all string methods, see the JavaScript string methods cheat sheet.

MethodNegative indicesSwaps args if start > end
slice()Supported (from end)No (returns empty)
substring()Treated as 0Yes

Ready to practice?

Start practicing JavaScript String Methods with spaced repetition

replace() only hits the first match with string patterns. Use replaceAll() for all matches—and if passing a regex, it must have the /g flag or it throws TypeError.

// You expect all underscores replaced...
"a_b_c".replace("_", "-")    // "a-b_c" (only first!)

// replaceAll fixes this
"a_b_c".replaceAll("_", "-") // "a-b-c"

The regex /g requirement

When using regex with replaceAll(), you MUST include the global flag:

"a1a2".replaceAll(/a/, "b")   // TypeError!
"a1a2".replaceAll(/a/g, "b")  // "b1b2"

This is intentional. It prevents the confusing case where replaceAll with a regex would only replace the first match.

When to use which

  • String pattern, replace all: replaceAll("old", "new")
  • String pattern, replace first: replace("old", "new")
  • Regex pattern, replace all: replace(/pattern/g, "new") or replaceAll(/pattern/g, "new")
  • Regex pattern, replace first: replace(/pattern/, "new")

Template literals support interpolation, multiline strings, and tagged templates. Tags don't auto-sanitize—your tag function must implement escaping if you're building HTML or SQL.

Template literals (backticks) support three features:

1. Interpolation

const name = "Alice";
const greeting = `Hello, ${name}!`; // "Hello, Alice!"

// Expressions work too
`Total: $${(price * 1.1).toFixed(2)}`

2. Multi-line strings

const html = `
  <div>
    <h1>${title}</h1>
    <p>${content}</p>
  </div>
`;

3. Tagged templates

A tag function receives the template parts separately:

function highlight(strings, ...values) {
  return strings.reduce((acc, str, i) => {
    const value = values[i] !== undefined ? `<mark>${values[i]}</mark>` : "";
    return acc + str + value;
  }, "");
}

const name = "Alice";
highlight`Hello, ${name}!`
// "Hello, <mark>Alice</mark>!"

Critical misconception: Tagged templates don't automatically sanitize or escape anything. The tag function must implement that logic. A call like html\

${input}
`is NOT safe unless yourhtml` tag escapes HTML entities.


JavaScript strings are sequences of UTF-16 code units, not Unicode characters:

"hello".length  // 5, as expected
"👍".length      // 2, not 1!
"👍🏽".length    // 4 (emoji + skin tone modifier)
"café".length   // 4 or 5 depending on encoding

Why this happens

UTF-16 represents characters outside the Basic Multilingual Plane (BMP) as "surrogate pairs": two 16-bit code units. Many emoji fall outside the BMP.

Counting actual characters (graphemes)

Use Intl.Segmenter for accurate character counts:

function graphemeLength(str) {
  const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
  return [...segmenter.segment(str)].length;
}

graphemeLength("hello")  // 5
graphemeLength("👍")      // 1
graphemeLength("👍🏽")    // 1 (emoji + modifier = 1 grapheme)

When this matters

  • Character limits in UI (Twitter, form fields)
  • Text truncation with ellipsis
  • Cursor positioning in text editors

Comparing strings with accents

// Wrong: byte comparison
["ä", "z", "a"].sort() // ["a", "z", "ä"] (ä after z!)

// Right: locale-aware
["ä", "z", "a"].sort((a, b) => a.localeCompare(b, "de"))
// ["a", "ä", "z"] (German order)

For repeated comparisons, use Intl.Collator

const collator = new Intl.Collator("de");
const sorted = names.sort(collator.compare); // Much faster for large arrays

normalize() for string equality

The same character can have multiple Unicode representations:

const a = "café";        // 'é' as single codepoint (U+00E9)
const b = "cafe\u0301";  // 'e' + combining acute accent

a === b           // false!
a.normalize() === b.normalize()  // true

Always normalize() user input before comparing or storing if accented characters are possible. If you're coming from Python, the Python vs JavaScript variables comparison covers how string handling differs between the two languages.


slugify (URL-safe string)

function slugify(str) {
  return str
    .toLowerCase()
    .normalize("NFD")                   // Decompose accents
    .replace(/[\u0300-\u036f]/g, "")   // Remove accent marks
    .replace(/[^a-z0-9]+/g, "-")        // Non-alphanumeric → dash
    .replace(/^-|-$/g, "");             // Trim leading/trailing dashes
}

slugify("Héllo Wörld!")  // "hello-world"

camelCase to kebab-case

These recipes rely on regex patterns. For a copy-paste reference of common regex syntax, see the JavaScript regex cheat sheet.

function toKebab(str) {
  return str
    .replace(/([a-z])([A-Z])/g, "$1-$2")  // Insert dash between lower-upper
    .replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")  // Handle acronyms
    .toLowerCase();
}

toKebab("helloWorld")    // "hello-world"
toKebab("XMLHttpRequest") // "xml-http-request"

Safe HTML escaping

function escapeHtml(str) {
  const entities = {
    "&": "&amp;",
    "<": "&lt;",
    ">": "&gt;",
    '"': "&quot;",
    "'": "&#39;"
  };
  return str.replace(/[&<>"']/g, c => entities[c]);
}

Truncate with ellipsis (grapheme-safe)

function truncate(str, maxLength) {
  const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
  const graphemes = [...segmenter.segment(str)];
  if (graphemes.length <= maxLength) return str;
  return graphemes.slice(0, maxLength - 1).map(g => g.segment).join("") + "…";
}

truncate("Hello 👍🏽!", 8)  // "Hello 👍🏽!" (fits)
truncate("Hello 👍🏽!", 7)  // "Hello …" (truncated correctly)

TaskMethod
Extract from endslice(-n)
Replace first matchreplace(str, new)
Replace all matchesreplaceAll(str, new)
Check containsincludes(str)
Check start/endstartsWith(), endsWith()
Trim whitespacetrim(), trimStart(), trimEnd()
Pad to lengthpadStart(n), padEnd(n)
Get last charat(-1)
Split to arraysplit(delimiter)
Join arrayarr.join(delimiter)
Locale sortlocaleCompare()
Character countIntl.Segmenter

  • MDN: String
  • MDN: slice()
  • MDN: substring()
  • MDN: replace()
  • MDN: replaceAll()
  • MDN: Template literals
  • MDN: Intl.Segmenter
  • MDN: String.prototype.normalize()
  • MDN: localeCompare()

When to Use JavaScript String Methods

  • Format user-facing messages with template literals.
  • Parse and clean text input with split/trim/replace.
  • Use replaceAll (or regex with /g) for structured transformations.
  • Handle Unicode safely when character count matters (emoji, accents).
  • Use tagged templates for DSLs (HTML escaping, SQL, CSS-in-JS).

Check Your Understanding: JavaScript String Methods

Prompt

Convert "helloWorld" to "hello-world". Handle "XMLHttpRequest" sensibly.

What a strong answer looks like

Insert dashes at case boundaries, then lowercase. A naive regex `str.replace(/([A-Z])/g, "-$1").toLowerCase()` produces `x-m-l-http-request`. For proper acronym handling, use two passes: `str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").toLowerCase()` → `xml-http-request`.

What You'll Practice: JavaScript String Methods

Template literals (interpolation, multiline strings)Tagged template literals (how tags receive strings + values)slice() vs substring() (negative indices, argument swapping)replace() vs replaceAll() + regex replacematch() vs matchAll() (capturing groups, iterators)split() and join()includes(), indexOf(), startsWith(), endsWith()padStart(), padEnd(), trim(), trimStart(), trimEnd()toUpperCase(), toLowerCase()repeat(), at() (modern negative indexing)Unicode basics: length caveat, normalize(), localeCompare()Intl.Segmenter for grapheme counting

Common JavaScript String Methods Pitfalls

  • Strings are immutable (methods return new strings, not modifications)
  • Confusing slice() vs substring() (use slice for predictability)
  • replace() only replaces first match for string patterns
  • replaceAll() with regex requires /g flag or throws TypeError
  • str.length counts UTF-16 code units, not characters (emoji surprise)
  • Tagged templates don't automatically escape/sanitize (the tag function must do that)
  • Comparing strings with accents without normalize() or localeCompare()
  • Using indexOf() !== -1 when includes() is clearer

JavaScript String Methods FAQ

slice() or substring()?

`slice()` supports negative indices (counting from the end); `substring()` clamps negatives to 0 and may swap arguments if start > end. Prefer `slice()`. It's more predictable and matches array behavior.

replace() or replaceAll()?

`replace()` only replaces the first match when given a string pattern. `replaceAll()` replaces all matches. If you use `replace()` with a regex, add the `/g` flag to replace all.

Why does replaceAll(/pattern/) throw a TypeError?

When you pass a RegExp to `replaceAll()`, it must have the global (`/g`) flag. Without it, JavaScript throws a TypeError. This prevents accidentally replacing only the first match when you expected all.

Are strings mutable in JavaScript?

No. All string methods return new strings. They never modify the original. You need to reassign: `str = str.toUpperCase()`, not just `str.toUpperCase()`.

Why is str.length wrong for emoji?

`length` returns the number of UTF-16 code units, not user-perceived characters. Many emoji (like 👍🏽) use multiple code units. `"👍".length` is 2, `"👍🏽".length` is 4.

How do I count user-perceived characters (graphemes)?

Use `Intl.Segmenter` with `granularity: "grapheme"`: `[...new Intl.Segmenter("en", { granularity: "grapheme" }).segment(str)].length`. This correctly counts emoji, accented characters, and other multi-codepoint sequences as single characters.

How do I compare/sort strings with accents correctly?

Use `localeCompare()` for comparisons or `Intl.Collator` for repeated sorting. These handle locale-specific rules: `"ä".localeCompare("z", "de")` returns negative (ä before z in German), positive in Swedish.

Why do two strings that look identical fail equality checks?

Unicode can represent the same glyph multiple ways (composed vs decomposed). Use `str.normalize()` to canonicalize before comparing: `"café".normalize() === "cafe\u0301".normalize()`.

What are tagged template literals for?

Tagged templates let you process template strings with a function. The tag receives an array of string literals and the interpolated values separately. Use cases: HTML escaping, SQL parameterization, CSS-in-JS, i18n. The tag does NOT automatically sanitize. You must write that logic.

How do I safely build HTML with template literals?

Template literals don't escape HTML by default. Either use a tagged template that escapes (like lit-html), or escape manually: `str.replace(/[&<>"']/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",""":"&quot;","'":"&#39;"})[c])`.

How do I check if a string contains a substring?

Use `includes()`: `"hello".includes("ell")` returns `true`. For the position, use `indexOf()` which returns -1 if not found. Prefer `includes()` for existence checks. It's clearer than `indexOf() !== -1`.

How do I check if a string starts or ends with a value?

Use `startsWith()` and `endsWith()`. Both accept an optional position parameter: `"hello".startsWith("ell", 1)` returns true.

How do I get the last character of a string?

Use `str.at(-1)` (ES2022). This is cleaner than `str[str.length - 1]` or `str.charAt(str.length - 1)`. The `at()` method supports negative indices just like `slice()`.

How do I write multiline strings?

Use template literals (backticks): `` `line1\nline2` `` preserves the actual newlines. For older code, you'd concatenate strings or use `\n`. Template literals also allow embedded expressions: `` `Hello ${name}` ``.

JavaScript String Methods Syntax Quick Reference

Template literal
const name = "Alice";
const greeting = `Hello, ${name}!`;
slice vs substring
"abcd".slice(-2)      // "cd" (last 2)
"abcd".substring(-2) // "abcd" (negatives → 0)
replace vs replaceAll
"a_a_a".replace("_", "-")       // "a-a_a" (first only)
"a_a_a".replaceAll("_", "-")  // "a-a-a" (all)
replaceAll regex gotcha
"a1a2".replaceAll(/a/, "b")   // TypeError!
"a1a2".replaceAll(/a/g, "b") // "b1b2"
Unicode length surprise
"hello".length  // 5
"👍".length      // 2 (UTF-16 code units)
"👍🏽".length    // 4 (emoji + skin tone)
Grapheme counting (Intl.Segmenter)
const seg = new Intl.Segmenter("en", { granularity: "grapheme" });
[...seg.segment("👍🏽")].length; // 1
Tagged template
function html(strings, ...values) {
  return strings.reduce((acc, str, i) =>
    acc + str + (values[i] ? escape(values[i]) : ""), "");
}
const input = "<script>alert(1)</script>";
const safe = html`<div>${input}</div>`;
Locale-aware comparison
"ä".localeCompare("z", "de") // negative (ä < z in German)
"ä".localeCompare("z", "sv") // positive (ä > z in Swedish)
normalize() for comparison
const a = "café";           // composed
const b = "cafe\u0301";     // decomposed
a === b                      // false
a.normalize() === b.normalize() // true
Practical: trim + split
const text = "  Hello World  ";
const clean = text.trim().toLowerCase();
const sentence = "one two three";
const words = sentence.split(" ").filter(Boolean);
Practical: pad for alignment
"42".padStart(5, "0")  // "00042"
"hi".padEnd(10, ".")   // "hi........"
at() for negative indexing
"hello".at(-1)  // "o" (last char)
"hello".at(-2)  // "l"

JavaScript String Methods Sample Exercises

Example 1Difficulty: 1/5

Get the length of string `item_name`.

item_name.length
Example 2Difficulty: 2/5

Create a string with "Line 1" and "Line 2" on separate lines using backticks.

`Line 1
Line 2`
Example 3Difficulty: 2/5

Create a string "Total: ..." with the result of `price * quantity`.

`Total: ${price * quantity}`

+ 14 more exercises

Quick Reference
JavaScript String Methods Cheat Sheet →

Copy-ready syntax examples for quick lookup

Also in Other Languages

Python String Methods Practice: f-strings, split, join, strip, slicingRust Strings Practice: String vs &str, Ownership & Methods

Start practicing JavaScript String Methods

Free daily exercises with spaced repetition. No credit card required.

← Back to JavaScript Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.