substring()

ES3+

Returns the part of a string between two indices, swapping the arguments automatically if the start is greater than the end, and treating negative or NaN indices as 0. The original string is never modified. It behaves like slice() for well-ordered, in-range arguments but differs in how it normalizes unusual ones.

Syntax

string.substring(indexStart, indexEnd)

Parameters

indexStart number

Index of the first character to include

indexEnd number optional

Index of the first character to exclude

Return Value

string

A new string containing the specified part

Examples

JavaScript
const str = 'JavaScript';
console.log(str.substring(0, 4));
console.log(str.substring(4));
Output:
// 'Java' 'Script'

📌 When to Use

Use substring() when you are extracting by position and its forgiving argument handling is actually what you want. Its signature behavior is that substring(5, 0) and substring(0, 5) return the same text: if the start index is greater than the end index, the two are silently swapped. That is genuinely convenient when the bounds come from sources that may arrive in either order, such as the anchor and focus of a text selection, where the user may have dragged the mouse right-to-left. It also clamps negative values and NaN to 0 instead of counting from the end, so an uninitialized or failed calculation degrades to 'from the beginning' rather than producing a surprising suffix. In modern codebases, though, slice() is the conventional default because its negative-index support is useful and its stricter behavior surfaces bugs instead of hiding them; many style guides recommend picking one of the two and using it consistently. Prefer substring() over the legacy substr(), which takes a length rather than an end index and is formally deprecated (Annex B of the specification). If you find yourself relying on the argument-swapping behavior implicitly, add a comment, because readers who assume slice() semantics will misread the code.

⚠️ Common Mistakes

Assuming negative indices count from the end like slice(): substring() clamps any negative argument to 0, so str.substring(-3) returns the whole string, not the last three characters. This is the single most common source of bugs when switching between the two methods.

Confusing substring() with substr(): the deprecated substr() takes a start position and a LENGTH, while substring() takes two positions. substr(2, 3) returns three characters starting at index 2, but substring(2, 3) returns exactly one character. Mixing them up produces subtly wrong extractions.

Relying on argument swapping without realizing it: substring(end, start) quietly works, which means a genuine bug where your indices are reversed will never throw or return an empty string. With slice() the same mistake returns '' immediately and gets noticed; substring() can let it ship.

Forgetting the end index is exclusive: like slice(), substring(0, 4) on 'JavaScript' returns 'Java', four characters at positions 0 to 3. Off-by-one errors here are common when converting between index math and human descriptions like 'the first four characters'.

Indexing by UTF-16 code units and splitting a surrogate pair: emoji and other astral characters occupy two indices, so substring() can cut one in half, leaving a lone surrogate that renders as a broken glyph. Position math on user-generated text should respect code points or grapheme clusters.

Passing NaN by accident: any index computation that yields NaN, for example parseInt on bad input, is treated as 0 without warning, so the extraction silently starts at the beginning of the string instead of failing fast.

✅ Best Practices

Standardize on slice() for general extraction and reserve substring() for cases where clamping and argument swapping are the behavior you actually want; mixing the two styles in one codebase invites negative-index bugs.

Use substring() for text-selection logic where the start and end offsets can legitimately arrive in either order, and leave a comment noting that the automatic swap is intentional.

Never migrate code from substr() to substring() by renaming alone: convert the second argument from a length to an end index (start + length) or the results will be wrong.

Validate or clamp indices explicitly when they come from user input or arithmetic, rather than leaning on the silent NaN-to-0 conversion, so failures are detectable instead of masked.

When extracting around a search hit, compute the bounds once, for example index and index + term.length, and pass those to substring(); recomputing offsets inline in each call is where off-by-one mistakes creep in.

⚡ Performance Notes

substring() has the same cost profile as slice(): the work is proportional to the length of the extracted text, and engines like V8 frequently avoid copying at all by returning a sliced-string view into the parent. The argument normalization (clamping negatives, swapping reversed bounds) is a couple of comparisons and is unmeasurable in practice, so choosing between substring() and slice() is a question of semantics and readability, never speed. The one real performance consideration is shared with all extraction methods: a small substring can pin its large parent string in memory when the engine uses a view representation, which matters if you extract tiny tokens from huge documents and retain them long-term. In hot parsing loops, prefer computing indices once and extracting once over repeated nested substring() calls that each allocate an intermediate string.

🌍 Real World Example

Text Selection and Highlighting

Two text-editing utilities. highlightText() locates a search term case-insensitively, then uses three substring() calls to split the text into the part before the hit, the hit itself, and the part after, wrapping the middle piece in a mark tag for highlighting; extracting the match from the original text preserves its original casing. getSelectedText() returns the text between a selection start and end, and works even when the user dragged backwards so the start offset is larger than the end offset, precisely because substring() swaps reversed arguments automatically. That swap is the method's distinguishing feature put to legitimate use.

// Highlight search term in text
function highlightText(text, searchTerm) {
  const index = text.toLowerCase().indexOf(searchTerm.toLowerCase());
  if (index === -1) return text;

  const before = text.substring(0, index);
  const match = text.substring(index, index + searchTerm.length);
  const after = text.substring(index + searchTerm.length);

  return before + '<mark>' + match + '</mark>' + after;
}

const text = 'Welcome to JavaScript programming';
console.log(highlightText(text, 'JavaScript'));
// Output: 'Welcome to <mark>JavaScript</mark> programming'

// Extract text between selection indices
function getSelectedText(text, selectionStart, selectionEnd) {
  // substring automatically handles if start > end
  return text.substring(selectionStart, selectionEnd);
}

console.log(getSelectedText('Hello World', 0, 5)); // 'Hello'
console.log(getSelectedText('Hello World', 5, 0)); // 'Hello' (swapped)

Related Methods