String.prototype.substring()

The older sibling of slice, kept for compatibility. Its two differences are both forms of silent correction, which is why slice is the better default.

String methodES1 (1997)Live demo
Common call
s.substring(0, 10)
Returns
a new string; end is exclusive
Replaces
nothing — slice replaces IT
Watch out
negatives become 0, and a backwards range is swapped
string.substring(start[, end])
→ string

Demo

Live evaluation
Try:
Inputs
sstringthe source string
startnumberstart index
endnumberend index (exclusive)
Output
'abcdef'.substring(1, 3)
'bc'

The first case behaves exactly like slice, which is why the two get confused — on well-formed input they are identical. The differences appear at the edges. Given (3, 1) substring quietly reorders the arguments and returns 'bc', where slice would return an empty string. And a negative index is not counted from the end; it is clamped to 0, so substring(-3) returns the WHOLE string while slice(-3) returns the last three characters. The fourth case is the trap in practice: substring(0, -1) means substring(0, 0), an empty string, where the same call on slice drops the final character.

Parameters

NameTypeRequiredDescription
startnumberyesIndex to start at. Anything negative, or NaN, is treated as 0.
endnumberno (length)Index to stop BEFORE, also clamped to 0 at the bottom. If start is greater than end the two are swapped.

Return value

string — The characters between the two indices, end exclusive. Negative and NaN arguments are treated as 0, and the pair is reordered if start is greater than end.

Common patterns

Prefer slice
Identical on valid input, predictable on invalid.
const part = s.slice(start, end);
When the swap is genuinely useful
Two positions from a text selection, in unknown order.
const selected = text.substring(anchorOffset, focusOffset);
Extract by length instead
What substr did, written with slice.
const chunk = s.slice(i, i + n);

Examples

1. Same as slice here
'abcdef'.substring(1, 3)
Returns
'bc'
2. Backwards is swapped
'abcdef'.substring(3, 1)
Returns
'bc'
3. slice is empty
'abcdef'.slice(3, 1)
Returns
''
4. Negative means 0
'abcdef'.substring(-3)
Returns
'abcdef'
5. slice counts back
'abcdef'.slice(-3)
Returns
'def'
6. substr takes a length
'abcdef'.substr(1, 3)
Returns
'bcd'

Pitfalls

1. substring(0, -1) does not drop the last character
The single most costly difference. The -1 is clamped to 0, making the call substring(0, 0) — an empty string. The identical-looking slice(0, -1) does what was intended, so code ported between the two breaks silently.
Empty
'abcdef'.substring(0, -1)
''
Use slice
'abcdef'.slice(0, -1)
'abcde'
2. The argument swap hides bugs
When start and end come from a calculation, a start that has drifted past the end is a logic error. substring quietly reorders and returns a plausible-looking string, so the mistake survives to production instead of surfacing as an obviously empty result.
Looks fine
'abcdef'.substring(3, 1)
'bc' // silently reordered
slice exposes it
'abcdef'.slice(3, 1)
'' // clearly wrong
3. substr is a different method, and deprecated
Its second argument is a LENGTH, not an end index, so substr(1, 3) returns three characters where substring(1, 3) returns two. It lives in Annex B — normatively optional, kept only because the web depends on it — and should not appear in new code.
Length, not end
'abcdef'.substr(1, 3)
'bcd' // three characters
slice with arithmetic
'abcdef'.slice(1, 1 + 3)
'bcd'
4. NaN becomes 0, so a bad index is invisible
An index that came out as NaN — from a failed parseInt, say — is coerced to 0 rather than throwing, so the result is a substring starting at the beginning. slice does the same, which is why neither method will tell you your arithmetic failed.
Silently from 0
'abcdef'.substring(NaN, 3)
'abc'
Validate first
if (Number.isInteger(i)) s.substring(i, 3);
explicit

When to use

Use it
  • Two offsets whose order is genuinely unknown — a text selection
  • Maintaining existing code that already uses it
Reach for something else
  • New code → slice, which handles negatives and does not hide bugs
  • Counting from the end → slice, since negatives are clamped here
  • You have a start and a length → slice(i, i + n)
  • substr specifically → deprecated Annex B, use slice

Notes

Complexity
O(n) in the length of the result
Return
A new string; the original is untouched
CPython impl
V8: Builtins-string-substring
Memory
Like slice, engines may share the original buffer
Thread-safe
Single-threaded; the string is only read

FAQ

Two things, both at the edges. First, if start is greater than end, substring swaps them while slice returns an empty string. Second, substring clamps negative indices to 0 while slice counts them from the end. On any call with sensible ascending non-negative indices the two are identical.

'abcdef'.substring(3, 1);   // 'bc'   — swapped
'abcdef'.slice(3, 1);       // ''     — empty
'abcdef'.substring(-3);     // 'abcdef'
'abcdef'.slice(-3);         // 'def'

History

ES1
substring present from the first version of the language.
ES3
slice added with negative-index support, superseding it for most uses.
ES5
substr documented in Annex B as a legacy feature required for web compatibility.