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
Name
Type
Required
Description
start
number
yes
Index to start at. Anything negative, or NaN, is treated as 0.
end
number
no (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.
constpart = s.slice(start, end);
When the swap is genuinely useful
Two positions from a text selection, in unknown order.
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
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.